1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-06-30 06:30:11 +02:00

feat: add add new config strategy to frontend

This commit is contained in:
Elias Schneider 2022-11-28 17:50:36 +01:00
parent 1b5e53ff7e
commit 493705e4ef
20 changed files with 183 additions and 102 deletions

View File

@ -1,18 +0,0 @@
# Read what every environment variable does: https://github.com/stonith404/pingvin-share#environment-variables
# General
APP_URL=http://localhost:3000
SHOW_HOME_PAGE=true
ALLOW_REGISTRATION=true
ALLOW_UNAUTHENTICATED_SHARES=false
MAX_FILE_SIZE=1000000000
# Security
JWT_SECRET=long-random-string
# Email
EMAIL_RECIPIENTS_ENABLED=false
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_EMAIL=pingvin-share@example.com
SMTP_PASSWORD=example

View File

@ -1,15 +0,0 @@
# General
APP_URL=http://localhost:3000
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=5000000000
ALLOW_UNAUTHENTICATED_SHARES=false
# Security
JWT_SECRET=random-string
# Email
EMAIL_RECIPIENTS_ENABLED=false
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_EMAIL=pingvin-share@example.com
SMTP_PASSWORD=example

View File

@ -5,6 +5,7 @@ import { User } from "@prisma/client";
export class AdministratorGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const { user }: { user: User } = context.switchToHttp().getRequest();
if (!user) return false;
return user.isAdministrator;
}
}

View File

@ -1,6 +1,9 @@
import { Controller, Get } from "@nestjs/common";
import { Body, Controller, Get, Param, Patch, UseGuards } from "@nestjs/common";
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
import { ConfigService } from "./config.service";
import { AdminConfigDTO } from "./dto/adminConfig.dto";
import { ConfigDTO } from "./dto/config.dto";
import UpdateConfigDTO from "./dto/updateConfig.dto";
@Controller("configs")
export class ConfigController {
@ -8,11 +11,22 @@ export class ConfigController {
@Get()
async list() {
return new ConfigDTO().fromList(await this.configService.list())
return new ConfigDTO().fromList(await this.configService.list());
}
@Get("admin")
@UseGuards(AdministratorGuard)
async listForAdmin() {
return await this.configService.listForAdmin();
return new AdminConfigDTO().fromList(
await this.configService.listForAdmin()
);
}
@Patch("admin/:key")
@UseGuards(AdministratorGuard)
async update(@Param("key") key: string, @Body() data: UpdateConfigDTO) {
return new AdminConfigDTO().from(
await this.configService.update(key, data.value)
);
}
}

View File

@ -1,4 +1,9 @@
import { Inject, Injectable } from "@nestjs/common";
import {
BadRequestException,
Inject,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { Config } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
@ -38,4 +43,23 @@ export class ConfigService {
return configVariable;
});
}
async update(key: string, value: string | number | boolean) {
const configVariable = await this.prisma.config.findUnique({
where: { key },
});
if (!configVariable || configVariable.locked)
throw new NotFoundException("Config variable not found");
if (typeof value != configVariable.type)
throw new BadRequestException(
`Config variable must be of type ${configVariable.type}`
);
return await this.prisma.config.update({
where: { key },
data: { value: value.toString() },
});
}
}

View File

@ -0,0 +1,23 @@
import { Expose, plainToClass } from "class-transformer";
import { ConfigDTO } from "./config.dto";
export class AdminConfigDTO extends ConfigDTO {
@Expose()
default: string;
@Expose()
secret: boolean;
@Expose()
updatedAt: Date;
from(partial: Partial<AdminConfigDTO>) {
return plainToClass(AdminConfigDTO, partial, { excludeExtraneousValues: true });
}
fromList(partial: Partial<AdminConfigDTO>[]) {
return partial.map((part) =>
plainToClass(AdminConfigDTO, part, { excludeExtraneousValues: true })
);
}
}

View File

@ -0,0 +1,8 @@
import { IsNotEmpty } from "class-validator";
class UpdateConfigDTO {
@IsNotEmpty()
value: string | number | boolean;
}
export default UpdateConfigDTO;

View File

@ -1,5 +0,0 @@
SHOW_HOME_PAGE=true
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=1000000000
ALLOW_UNAUTHENTICATED_SHARES=false
EMAIL_RECIPIENTS_ENABLED=false

View File

@ -1,14 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
publicRuntimeConfig: {
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES,
EMAIL_RECIPIENTS_ENABLED: process.env.EMAIL_RECIPIENTS_ENABLED
}
}
const withPWA = require("next-pwa")({
dest: "public",
@ -16,4 +7,4 @@ const withPWA = require("next-pwa")({
});
module.exports = withPWA(nextConfig);
module.exports = withPWA();

View File

@ -9,15 +9,15 @@ import {
Title,
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import getConfig from "next/config";
import Link from "next/link";
import * as yup from "yup";
import useConfig from "../../hooks/config.hook";
import authService from "../../services/auth.service";
import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
const config = useConfig();
const validationSchema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
@ -55,7 +55,7 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
>
{mode == "signUp" ? "Sign up" : "Welcome back"}
</Title>
{publicRuntimeConfig.ALLOW_REGISTRATION == "true" && (
{config.get("allowRegistration") && (
<Text color="dimmed" size="sm" align="center" mt={5}>
{mode == "signUp"
? "You have an account already?"

View File

@ -11,15 +11,13 @@ import {
Transition,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import getConfig from "next/config";
import Link from "next/link";
import { ReactNode, useEffect, useState } from "react";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import Logo from "../Logo";
import ActionAvatar from "./ActionAvatar";
const { publicRuntimeConfig } = getConfig();
const HEADER_HEIGHT = 60;
type NavLink = {
@ -110,6 +108,8 @@ const useStyles = createStyles((theme) => ({
const NavBar = () => {
const user = useUser();
const config = useConfig();
const [opened, toggleOpened] = useDisclosure(false);
const authenticatedLinks = [
@ -130,7 +130,7 @@ const NavBar = () => {
]);
useEffect(() => {
if (publicRuntimeConfig.SHOW_HOME_PAGE == "true")
if (config.get("showHomePage"))
setUnauthenticatedLinks((array) => [
{
link: "/",
@ -139,7 +139,7 @@ const NavBar = () => {
...array,
]);
if (publicRuntimeConfig.ALLOW_REGISTRATION == "true")
if (config.get("allowRegistration"))
setUnauthenticatedLinks((array) => [
...array,
{

View File

@ -1,14 +1,12 @@
import { Button, Center, createStyles, Group, Text } from "@mantine/core";
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
import getConfig from "next/config";
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
import { TbCloudUpload, TbUpload } from "react-icons/tb";
import useConfig from "../../hooks/config.hook";
import { FileUpload } from "../../types/File.type";
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const useStyles = createStyles((theme) => ({
wrapper: {
position: "relative",
@ -40,12 +38,14 @@ const Dropzone = ({
isUploading: boolean;
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
}) => {
const config = useConfig();
const { classes } = useStyles();
const openRef = useRef<() => void>();
return (
<div className={classes.wrapper}>
<MantineDropzone
maxSize={parseInt(publicRuntimeConfig.MAX_FILE_SIZE!)}
maxSize={parseInt(config.get("maxFileSize"))}
onReject={(e) => {
toast.error(e[0].errors[0].message);
}}
@ -75,8 +75,7 @@ const Dropzone = ({
<Text align="center" size="sm" mt="xs" color="dimmed">
Drag&apos;n&apos;drop files here to start your share. We can accept
only files that are less than{" "}
{byteStringToHumanSizeString(publicRuntimeConfig.MAX_FILE_SIZE)} in
size.
{byteStringToHumanSizeString(config.get("maxFileSize"))} in size.
</Text>
</div>
</MantineDropzone>

View File

@ -18,7 +18,6 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import getConfig from "next/config";
import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb";
import * as yup from "yup";
@ -26,11 +25,13 @@ import shareService from "../../../services/share.service";
import { ShareSecurity } from "../../../types/share.type";
import ExpirationPreview from "../ExpirationPreview";
const { publicRuntimeConfig } = getConfig();
const showCreateUploadModal = (
modals: ModalsContextProps,
isSignedIn: boolean,
options: {
isUserSignedIn: boolean;
allowUnauthenticatedShares: boolean;
emailRecipientsEnabled: boolean;
},
uploadCallback: (
id: string,
expiration: string,
@ -42,7 +43,7 @@ const showCreateUploadModal = (
title: <Title order={4}>Share</Title>,
children: (
<CreateUploadModalBody
isSignedIn={isSignedIn}
options={options}
uploadCallback={uploadCallback}
/>
),
@ -51,7 +52,7 @@ const showCreateUploadModal = (
const CreateUploadModalBody = ({
uploadCallback,
isSignedIn,
options,
}: {
uploadCallback: (
id: string,
@ -59,12 +60,16 @@ const CreateUploadModalBody = ({
recipients: string[],
security: ShareSecurity
) => void;
isSignedIn: boolean;
options: {
isUserSignedIn: boolean;
allowUnauthenticatedShares: boolean;
emailRecipientsEnabled: boolean;
};
}) => {
const modals = useModals();
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(
publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true"
options.emailRecipientsEnabled
);
const validationSchema = yup.object().shape({
@ -93,7 +98,7 @@ const CreateUploadModalBody = ({
});
return (
<Group>
{showNotSignedInAlert && !isSignedIn && (
{showNotSignedInAlert && !options.isUserSignedIn && (
<Alert
withCloseButton
onClose={() => setShowNotSignedInAlert(false)}
@ -225,7 +230,7 @@ const CreateUploadModalBody = ({
{ExpirationPreview({ form })}
</Text>
<Accordion>
{publicRuntimeConfig.EMAIL_RECIPIENTS_ENABLED == "true" && (
{options.emailRecipientsEnabled && (
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
<Accordion.Control>Email recipients</Accordion.Control>
<Accordion.Panel>

View File

@ -0,0 +1,14 @@
import { createContext, useContext } from "react";
import configService from "../services/config.service";
import Config from "../types/config.type";
export const ConfigContext = createContext<Config[] | null>(null);
const useConfig = () => {
const configVariables = useContext(ConfigContext) as Config[];
return {
get: (key: string) => configService.get(key, configVariables),
};
};
export default useConfig;

View File

@ -10,11 +10,14 @@ import { NotificationsProvider } from "@mantine/notifications";
import type { AppProps } from "next/app";
import { useEffect, useState } from "react";
import Header from "../components/navBar/NavBar";
import { ConfigContext } from "../hooks/config.hook";
import { UserContext } from "../hooks/user.hook";
import authService from "../services/auth.service";
import configService from "../services/config.service";
import userService from "../services/user.service";
import GlobalStyle from "../styles/global.style";
import globalStyle from "../styles/mantine.style";
import Config from "../types/config.type";
import { CurrentUser } from "../types/user.type";
import { GlobalLoadingContext } from "../utils/loading.util";
@ -24,9 +27,11 @@ function App({ Component, pageProps }: AppProps) {
const [colorScheme, setColorScheme] = useState<ColorScheme>();
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<CurrentUser | null>(null);
const [config, setConfig] = useState<Config[] | null>(null);
const getInitalData = async () => {
setIsLoading(true);
setConfig(await configService.getAll());
await authService.refreshAccessToken();
setUser(await userService.getCurrentUser());
setIsLoading(false);
@ -54,13 +59,15 @@ function App({ Component, pageProps }: AppProps) {
{isLoading ? (
<LoadingOverlay visible overlayOpacity={1} />
) : (
<UserContext.Provider value={user}>
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
<Header />
<Container>
<Component {...pageProps} />
</Container>
</UserContext.Provider>
<ConfigContext.Provider value={config}>
<UserContext.Provider value={user}>
<LoadingOverlay visible={isLoading} overlayOpacity={1} />
<Header />
<Container>
<Component {...pageProps} />
</Container>
</UserContext.Provider>{" "}
</ConfigContext.Provider>
)}
</GlobalLoadingContext.Provider>
</ModalsProvider>
@ -69,9 +76,4 @@ function App({ Component, pageProps }: AppProps) {
);
}
// Opts out of static site generation to use publicRuntimeConfig
App.getInitialProps = () => {
return {};
};
export default App;

View File

@ -1,17 +1,16 @@
import getConfig from "next/config";
import { useRouter } from "next/router";
import AuthForm from "../../components/auth/AuthForm";
import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
const { publicRuntimeConfig } = getConfig();
const SignUp = () => {
const config = useConfig();
const user = useUser();
const router = useRouter();
if (user) {
router.replace("/");
} else if (publicRuntimeConfig.ALLOW_REGISTRATION == "false") {
} else if (config.get("allowRegistration") == "false") {
router.replace("/auth/signIn");
} else {
return (

View File

@ -8,16 +8,14 @@ import {
ThemeIcon,
Title,
} from "@mantine/core";
import getConfig from "next/config";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { TbCheck } from "react-icons/tb";
import Meta from "../components/Meta";
import useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook";
const { publicRuntimeConfig } = getConfig();
const useStyles = createStyles((theme) => ({
inner: {
display: "flex",
@ -71,13 +69,14 @@ const useStyles = createStyles((theme) => ({
}));
export default function Home() {
const config = useConfig();
const user = useUser();
const { classes } = useStyles();
const router = useRouter();
if (user || publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true") {
if (user || config.get("allowUnauthenticatedShares")) {
router.replace("/upload");
} else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") {
} else if (!config.get("showHomePage")) {
router.replace("/auth/signIn");
} else {
return (

View File

@ -1,7 +1,6 @@
import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals";
import axios from "axios";
import getConfig from "next/config";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Meta from "../components/Meta";
@ -9,13 +8,13 @@ import Dropzone from "../components/upload/Dropzone";
import FileList from "../components/upload/FileList";
import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal";
import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal";
import useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook";
import shareService from "../services/share.service";
import { FileUpload } from "../types/File.type";
import { ShareSecurity } from "../types/share.type";
import toast from "../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
let share: any;
const Upload = () => {
@ -23,6 +22,7 @@ const Upload = () => {
const modals = useModals();
const user = useUser();
const config = useConfig();
const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false);
@ -95,7 +95,7 @@ const Upload = () => {
}
}
}, [files]);
if (!user && publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "false") {
if (!user && !config.get("allowUnauthenticatedShares")) {
router.replace("/");
} else {
return (
@ -106,7 +106,17 @@ const Upload = () => {
loading={isUploading}
disabled={files.length <= 0}
onClick={() =>
showCreateUploadModal(modals, user ? true : false, uploadFiles)
showCreateUploadModal(
modals,
{
isUserSignedIn: user ? true : false,
allowUnauthenticatedShares: config.get(
"allowUnauthenticatedShares"
),
emailRecipientsEnabled: config.get("emailRecipientsEnabled"),
},
uploadFiles
)
}
>
Share

View File

@ -0,0 +1,23 @@
import Config from "../types/config.type";
import api from "./api.service";
const getAll = async (): Promise<Config[]> => {
return (await api.get("/configs")).data;
};
const get = (key: string, configVariables: Config[]): any => {
const configVariable = configVariables.filter(
(variable) => variable.key == key
)[0];
if (!configVariable) throw new Error(`Config variable ${key} not found`);
if (configVariable.type == "number") return parseInt(configVariable.value);
if (configVariable.type == "boolean") return configVariable.value == "true";
if (configVariable.type == "string") return configVariable.value;
};
export default {
getAll,
get,
};

View File

@ -0,0 +1,7 @@
type Config = {
key: string;
value: string;
type: string;
};
export default Config;