Compare commits

...

7 Commits

Author SHA1 Message Date
Elias Schneider
0c2a62b0ca release: 0.12.0 2023-03-10 09:40:19 +01:00
Elias Schneider
452c635933 chore: dump packages 2023-03-10 09:40:09 +01:00
Elias Schneider
0455ba1bc1 chore: upgrade mantine to v6 2023-03-10 09:01:33 +01:00
Elias Schneider
3ad6b03b6b fix: home page shown even if disabled 2023-03-10 08:40:32 +01:00
Elias Schneider
91c3525b15 chore: add sharp for image optimizations 2023-03-08 17:47:36 +01:00
Elias Schneider
8403d7e14d feat: ability to change logo in frontend 2023-03-08 14:47:41 +01:00
Elias Schneider
8f71fd3435 fix: crypto is not defined 2023-03-08 13:10:10 +01:00
28 changed files with 3227 additions and 1119 deletions

View File

@@ -1,3 +1,16 @@
## [0.12.0](https://github.com/stonith404/pingvin-share/compare/v0.11.1...v0.12.0) (2023-03-10)
### Features
* ability to change logo in frontend ([8403d7e](https://github.com/stonith404/pingvin-share/commit/8403d7e14ded801c3842a9b3fd87c3f6824c519e))
### Bug Fixes
* crypto is not defined ([8f71fd3](https://github.com/stonith404/pingvin-share/commit/8f71fd343506506532c1a24a4c66a16b1021705f))
* home page shown even if disabled ([3ad6b03](https://github.com/stonith404/pingvin-share/commit/3ad6b03b6bd80168870049582683077b689fa548))
### [0.11.1](https://github.com/stonith404/pingvin-share/compare/v0.11.0...v0.11.1) (2023-03-05) ### [0.11.1](https://github.com/stonith404/pingvin-share/compare/v0.11.0...v0.11.1) (2023-03-05)

View File

@@ -96,18 +96,7 @@ docker compose up -d
### Custom branding ### Custom branding
#### Name You can change the name and the logo of the app by visiting the admin configuration page.
You can change the name of the app by visiting the admin configuration page and changing the `App Name`.
#### Logo
You can change the logo of the app by replacing the images in the `/data/images` (or with the standalone installation `/frontend/public/img`) folder with your own logo. The folder contains the following images:
- `logo.png` - The logo in the header and home page
- `favicon.png` - The favicon
- `opengraph.png` - The image used for sharing on social media
- `icons/*` - The icons used for the PWA
## 🖤 Contribute ## 🖤 Contribute

2052
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share-backend", "name": "pingvin-share-backend",
"version": "0.11.1", "version": "0.12.0",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"dev": "cross-env NODE_ENV=development nest start --watch", "dev": "cross-env NODE_ENV=development nest start --watch",
@@ -13,65 +13,68 @@
"seed": "ts-node prisma/seed/config.seed.ts" "seed": "ts-node prisma/seed/config.seed.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^9.2.1", "@nestjs/common": "^9.3.9",
"@nestjs/config": "^2.2.0", "@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.2.1", "@nestjs/core": "^9.3.9",
"@nestjs/jwt": "^10.0.1", "@nestjs/jwt": "^10.0.2",
"@nestjs/passport": "^9.0.0", "@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.2.1", "@nestjs/platform-express": "^9.3.9",
"@nestjs/schedule": "^2.1.0", "@nestjs/schedule": "^2.2.0",
"@nestjs/swagger": "^6.2.1", "@nestjs/swagger": "^6.2.1",
"@nestjs/throttler": "^3.1.0", "@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.8.1", "@prisma/client": "^4.11.0",
"archiver": "^5.3.1", "archiver": "^5.3.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"body-parser": "^1.20.1", "body-parser": "^1.20.2",
"clamscan": "^2.1.2", "clamscan": "^2.1.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.14.0",
"content-disposition": "^0.5.4", "content-disposition": "^0.5.4",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"nodemailer": "^6.9.0", "nodemailer": "^6.9.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"qrcode-svg": "^1.1.0", "qrcode-svg": "^1.1.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^4.0.4", "rimraf": "^4.4.0",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"sharp": "^0.31.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.1.8", "@nestjs/cli": "^9.2.0",
"@nestjs/schematics": "^9.0.4", "@nestjs/schematics": "^9.0.4",
"@nestjs/testing": "^9.2.1", "@nestjs/testing": "^9.3.9",
"@types/archiver": "^5.3.1", "@types/archiver": "^5.3.1",
"@types/clamscan": "^2.0.4", "@types/clamscan": "^2.0.4",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",
"@types/express": "^4.17.15", "@types/express": "^4.17.17",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/node": "^18.11.18", "@types/multer": "^1.4.7",
"@types/node": "^18.15.0",
"@types/nodemailer": "^6.4.7", "@types/nodemailer": "^6.4.7",
"@types/passport-jwt": "^3.0.8", "@types/passport-jwt": "^3.0.8",
"@types/qrcode-svg": "^1.1.1", "@types/qrcode-svg": "^1.1.1",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^5.54.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.31.0", "eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.7.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"newman": "^5.3.2", "newman": "^5.3.2",
"prettier": "^2.8.2", "prettier": "^2.8.4",
"prisma": "^4.9.0", "prisma": "^4.11.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"typescript": "^4.9.4", "typescript": "^4.9.5",
"wait-on": "^7.0.1" "wait-on": "^7.0.1"
} }
} }

View File

@@ -42,7 +42,7 @@ export class AuthTotpService {
throw new UnauthorizedException("Invalid login token"); throw new UnauthorizedException("Invalid login token");
if (token.expiresAt < new Date()) if (token.expiresAt < new Date())
throw new UnauthorizedException("Login token expired"); throw new UnauthorizedException("Login token expired", "token_expired");
// Check the TOTP code // Check the TOTP code
const { totpSecret } = await this.prisma.user.findUnique({ const { totpSecret } = await this.prisma.user.findUnique({

View File

@@ -1,12 +1,17 @@
import { import {
Body, Body,
Controller, Controller,
FileTypeValidator,
Get, Get,
Param, Param,
ParseFilePipe,
Patch, Patch,
Post, Post,
UploadedFile,
UseGuards, UseGuards,
UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { SkipThrottle } from "@nestjs/throttler"; import { SkipThrottle } from "@nestjs/throttler";
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";
@@ -16,11 +21,13 @@ 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 { TestEmailDTO } from "./dto/testEmail.dto";
import UpdateConfigDTO from "./dto/updateConfig.dto"; import UpdateConfigDTO from "./dto/updateConfig.dto";
import { LogoService } from "./logo.service";
@Controller("configs") @Controller("configs")
export class ConfigController { export class ConfigController {
constructor( constructor(
private configService: ConfigService, private configService: ConfigService,
private logoService: LogoService,
private emailService: EmailService private emailService: EmailService
) {} ) {}
@@ -51,4 +58,18 @@ export class ConfigController {
async testEmail(@Body() { email }: TestEmailDTO) { async testEmail(@Body() { email }: TestEmailDTO) {
await this.emailService.sendTestMail(email); await this.emailService.sendTestMail(email);
} }
@Post("admin/logo")
@UseInterceptors(FileInterceptor("file"))
@UseGuards(JwtGuard, AdministratorGuard)
async uploadLogo(
@UploadedFile(
new ParseFilePipe({
validators: [new FileTypeValidator({ fileType: "image/png" })],
})
)
file: Express.Multer.File
) {
return await this.logoService.create(file.buffer);
}
} }

View File

@@ -3,6 +3,7 @@ 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";
import { LogoService } from "./logo.service";
@Global() @Global()
@Module({ @Module({
@@ -16,6 +17,7 @@ import { ConfigService } from "./config.service";
inject: [PrismaService], inject: [PrismaService],
}, },
ConfigService, ConfigService,
LogoService,
], ],
controllers: [ConfigController], controllers: [ConfigController],
exports: [ConfigService], exports: [ConfigService],

View File

@@ -0,0 +1,32 @@
import { Injectable } from "@nestjs/common";
import * as fs from "fs";
import * as sharp from "sharp";
const IMAGES_PATH = "../frontend/public/img";
@Injectable()
export class LogoService {
async create(file: Buffer) {
fs.writeFileSync(`${IMAGES_PATH}/logo.png`, file, "binary");
this.createFavicon(file);
this.createPWAIcons(file);
}
async createFavicon(file: Buffer) {
const resized = await sharp(file).resize(16).toBuffer();
fs.promises.writeFile(`${IMAGES_PATH}/favicon.ico`, resized, "binary");
}
async createPWAIcons(file: Buffer) {
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
for (const size of sizes) {
const resized = await sharp(file).resize(size).toBuffer();
fs.promises.writeFile(
`${IMAGES_PATH}/icons/icon-${size}x${size}.png`,
resized,
"binary"
);
}
}
}

View File

@@ -1,6 +1,7 @@
import { BadRequestException, Injectable } from "@nestjs/common"; import { BadRequestException, Injectable } from "@nestjs/common";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import * as argon from "argon2"; import * as argon from "argon2";
import * as crypto from "crypto";
import { EmailService } from "src/email/email.service"; import { EmailService } from "src/email/email.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { CreateUserDTO } from "./dto/createUser.dto"; import { CreateUserDTO } from "./dto/createUser.dto";

View File

@@ -4,7 +4,15 @@ const { version } = require('./package.json');
const withPWA = require("next-pwa")({ const withPWA = require("next-pwa")({
dest: "public", dest: "public",
disable: process.env.NODE_ENV == "development", disable: process.env.NODE_ENV === "development",
reloadOnOnline: false,
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: 'NetworkOnly',
},
],
reloadOnOnline: false,
}); });
module.exports = withPWA({ module.exports = withPWA({

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share-frontend", "name": "pingvin-share-frontend",
"version": "0.11.1", "version": "0.12.0",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@@ -9,43 +9,44 @@
"format": "prettier --write \"src/**/*.ts*\"" "format": "prettier --write \"src/**/*.ts*\""
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@mantine/core": "^5.10.0", "@mantine/core": "^6.0.1",
"@mantine/dropzone": "^5.10.0", "@mantine/dropzone": "^6.0.1",
"@mantine/form": "^5.10.0", "@mantine/form": "^6.0.1",
"@mantine/hooks": "^5.10.0", "@mantine/hooks": "^6.0.1",
"@mantine/modals": "^5.10.0", "@mantine/modals": "^6.0.1",
"@mantine/next": "^5.10.0", "@mantine/next": "^6.0.1",
"@mantine/notifications": "^5.10.0", "@mantine/notifications": "^6.0.1",
"axios": "^1.2.2", "axios": "^1.3.4",
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^4.11.2", "jose": "^4.13.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.1.2", "next": "^13.2.4",
"next-cookies": "^2.0.3", "next-cookies": "^2.0.3",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"p-limit": "^4.0.0", "p-limit": "^4.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.7.1", "react-icons": "^4.8.0",
"yup": "^0.32.11" "sharp": "^0.31.3",
"yup": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/node": "18.11.18", "@types/node": "18.15.0",
"@types/react": "18.0.26", "@types/react": "18.0.28",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.11",
"axios": "^1.2.2", "axios": "^1.3.4",
"eslint": "8.31.0", "eslint": "8.35.0",
"eslint-config-next": "^13.1.2", "eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.7.0",
"prettier": "^2.8.2", "prettier": "^2.8.4",
"tar": "^6.1.13", "tar": "^6.1.13",
"typescript": "^4.9.4" "typescript": "^4.9.5"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,39 @@
import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { Dispatch, SetStateAction } from "react";
import { TbUpload } from "react-icons/tb";
const LogoConfigInput = ({
logo,
setLogo,
}: {
logo: File | null;
setLogo: Dispatch<SetStateAction<File | null>>;
}) => {
const isMobile = useMediaQuery("(max-width: 560px)");
return (
<Group position="apart">
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
<Title order={6}>Logo</Title>
<Text color="dimmed" size="sm" mb="xs">
Change your logo by uploading a new image. The image must be a PNG and
should have the format 1:1.
</Text>
</Stack>
<Stack></Stack>
<Box style={{ width: isMobile ? "100%" : "50%" }}>
<FileInput
clearable
icon={<TbUpload size={14} />}
value={logo}
onChange={(v) => setLogo(v)}
accept=".png"
placeholder="Pick image"
/>
</Box>
</Group>
);
};
export default LogoConfigInput;

View File

@@ -32,11 +32,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
emailOrUsername: yup.string().required(), emailOrUsername: yup.string().required(),
password: yup.string().min(8).required(), password: yup.string().min(8).required(),
totp: yup.string().when("totpRequired", {
is: true,
then: yup.string().min(6).max(6).required(),
otherwise: yup.string(),
}),
}); });
const form = useForm({ const form = useForm({
@@ -79,8 +74,8 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
router.replace(redirectPath); router.replace(redirectPath);
}) })
.catch((error) => { .catch((error) => {
if (error?.response?.data?.message == "Login token expired") { if (error?.response?.data?.error == "share_password_required") {
toast.error("Login token expired"); toast.axiosError(error);
// Refresh the page to start over // Refresh the page to start over
window.location.reload(); window.location.reload();
} }

View File

@@ -34,8 +34,10 @@ const FileSizeInput = ({
label={label} label={label}
value={size} value={size}
onChange={(value) => { onChange={(value) => {
setSize(value!); if (value) {
onChange(unitAndSizeToByte(unit, value!)); setSize(value);
onChange(unitAndSizeToByte(unit, value));
}
}} }}
/> />
</Col> </Col>

View File

@@ -1,14 +1,13 @@
import React from "react";
import { import {
createStyles,
Title,
Text,
Button, Button,
Container, Container,
createStyles,
Group, Group,
Text,
Title,
} from "@mantine/core"; } from "@mantine/core";
import Meta from "../components/Meta";
import Link from "next/link"; import Link from "next/link";
import Meta from "../components/Meta";
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
root: { root: {
@@ -21,7 +20,7 @@ const useStyles = createStyles((theme) => ({
fontWeight: 900, fontWeight: 900,
fontSize: 220, fontSize: 220,
lineHeight: 1, lineHeight: 1,
marginBottom: theme.spacing.xl * 1.5, marginBottom: `calc(${theme.spacing.xl} * 100)`,
color: theme.colors.gray[2], color: theme.colors.gray[2],
[theme.fn.smallerThan("sm")]: { [theme.fn.smallerThan("sm")]: {
@@ -32,7 +31,7 @@ const useStyles = createStyles((theme) => ({
description: { description: {
maxWidth: 500, maxWidth: 500,
margin: "auto", margin: "auto",
marginBottom: theme.spacing.xl * 1.5, marginBottom: `calc(${theme.spacing.xl} * 100)`,
}, },
})); }));

View File

@@ -6,7 +6,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useColorScheme } from "@mantine/hooks"; import { useColorScheme } from "@mantine/hooks";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
import { NotificationsProvider } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import axios from "axios"; import axios from "axios";
import { getCookie, setCookie } from "cookies-next"; import { getCookie, setCookie } from "cookies-next";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
@@ -76,40 +76,39 @@ function App({ Component, pageProps }: AppProps) {
toggleColorScheme={toggleColorScheme} toggleColorScheme={toggleColorScheme}
> >
<GlobalStyle /> <GlobalStyle />
<NotificationsProvider> <Notifications />
<ModalsProvider> <ModalsProvider>
<ConfigContext.Provider <ConfigContext.Provider
value={{
configVariables,
refresh: async () => {
setConfigVariables(await configService.list());
},
}}
>
<UserContext.Provider
value={{ value={{
configVariables, user,
refresh: async () => { refreshUser: async () => {
setConfigVariables(await configService.list()); const user = await userService.getCurrentUser();
setUser(user);
return user;
}, },
}} }}
> >
<UserContext.Provider {excludeDefaultLayoutRoutes.includes(route) ? (
value={{ <Component {...pageProps} />
user, ) : (
refreshUser: async () => { <>
const user = await userService.getCurrentUser(); <Header />
setUser(user); <Container>
return user; <Component {...pageProps} />
}, </Container>
}} </>
> )}
{excludeDefaultLayoutRoutes.includes(route) ? ( </UserContext.Provider>
<Component {...pageProps} /> </ConfigContext.Provider>
) : ( </ModalsProvider>
<>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</>
)}
</UserContext.Provider>
</ConfigContext.Provider>
</ModalsProvider>
</NotificationsProvider>
</ColorSchemeProvider> </ColorSchemeProvider>
</MantineProvider> </MantineProvider>
); );

View File

@@ -12,14 +12,8 @@ export default class _Document extends Document {
<Head> <Head>
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<link <link rel="apple-touch-icon" href="/img/icons/icon-128x128.png" />
rel="apple-touch-icon"
href="/img/icons/icon-white-128x128.png"
/>
<meta property="og:image" content="/img/opengraph.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="/img/opengraph.png" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<meta name="theme-color" content="#46509e" /> <meta name="theme-color" content="#46509e" />
</Head> </Head>

View File

@@ -16,6 +16,7 @@ import { useEffect, useState } from "react";
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput"; import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader"; import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar"; import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
import LogoConfigInput from "../../../components/admin/configuration/LogoConfigInput";
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton"; import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
import CenterLoader from "../../../components/core/CenterLoader"; import CenterLoader from "../../../components/core/CenterLoader";
import Meta from "../../../components/Meta"; import Meta from "../../../components/Meta";
@@ -36,22 +37,38 @@ export default function AppShellDemo() {
const isMobile = useMediaQuery("(max-width: 560px)"); const isMobile = useMediaQuery("(max-width: 560px)");
const config = useConfig(); const config = useConfig();
const categoryId = router.query.category as string; const categoryId = (router.query.category as string | undefined) ?? "general";
const [configVariables, setConfigVariables] = useState<AdminConfig[]>(); const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
const [updatedConfigVariables, setUpdatedConfigVariables] = useState< const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
UpdateConfig[] UpdateConfig[]
>([]); >([]);
const [logo, setLogo] = useState<File | null>(null);
const saveConfigVariables = async () => { const saveConfigVariables = async () => {
await configService if (logo) {
.updateMany(updatedConfigVariables) configService
.then(() => { .changeLogo(logo)
setUpdatedConfigVariables([]); .then(() => {
toast.success("Configurations updated successfully"); setLogo(null);
}) toast.success(
.catch(toast.axiosError); "Logo updated successfully. It may take a few minutes to update on the website."
config.refresh(); );
})
.catch(toast.axiosError);
}
if (updatedConfigVariables.length > 0) {
await configService
.updateMany(updatedConfigVariables)
.then(() => {
setUpdatedConfigVariables([]);
toast.success("Configurations updated successfully");
})
.catch(toast.axiosError);
config.refresh();
}
}; };
const updateConfigVariable = (configVariable: UpdateConfig) => { const updateConfigVariable = (configVariable: UpdateConfig) => {
@@ -129,6 +146,9 @@ export default function AppShellDemo() {
</Box> </Box>
</Group> </Group>
))} ))}
{categoryId == "general" && (
<LogoConfigInput logo={logo} setLogo={setLogo} />
)}
</Stack> </Stack>
<Group mt="lg" position="right"> <Group mt="lg" position="right">
{categoryId == "smtp" && ( {categoryId == "smtp" && (

View File

@@ -1,15 +0,0 @@
export function getServerSideProps() {
return {
redirect: {
permanent: false,
destination: "/admin/config/general",
},
props: {},
};
}
const Config = () => {
return null;
};
export default Config;

View File

@@ -41,7 +41,7 @@ const Admin = () => {
{ {
title: "Configuration", title: "Configuration",
icon: TbSettings, icon: TbSettings,
route: "/admin/config", route: "/admin/config/general",
}, },
]); ]);

View File

@@ -43,7 +43,7 @@ const Intro = () => {
<Text>Enough talked, have fun with Pingvin Share!</Text> <Text>Enough talked, have fun with Pingvin Share!</Text>
<Text mt="lg">How to you want to continue?</Text> <Text mt="lg">How to you want to continue?</Text>
<Stack> <Stack>
<Button href="/admin/config" component={Link}> <Button href="/admin/config/general" component={Link}>
Customize configuration Customize configuration
</Button> </Button>
<Button href="/" component={Link} variant="light"> <Button href="/" component={Link} variant="light">

View File

@@ -20,13 +20,13 @@ const useStyles = createStyles((theme) => ({
inner: { inner: {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
paddingTop: theme.spacing.xl * 4, paddingTop: `calc(${theme.spacing.md} * 4)`,
paddingBottom: theme.spacing.xl * 4, paddingBottom: `calc(${theme.spacing.md} * 4)`,
}, },
content: { content: {
maxWidth: 480, maxWidth: 480,
marginRight: theme.spacing.xl * 3, marginRight: `calc(${theme.spacing.md} * 3)`,
[theme.fn.smallerThan("md")]: { [theme.fn.smallerThan("md")]: {
maxWidth: "100%", maxWidth: "100%",

View File

@@ -125,7 +125,7 @@ const Upload = ({
toast.error( toast.error(
`${fileErrorCount} file(s) failed to upload. Trying again.`, `${fileErrorCount} file(s) failed to upload. Trying again.`,
{ {
disallowClose: true, withCloseButton: false,
autoClose: false, autoClose: false,
} }
); );

View File

@@ -46,6 +46,12 @@ const isNewReleaseAvailable = async () => {
return response.tag_name.replace("v", "") != process.env.VERSION; return response.tag_name.replace("v", "") != process.env.VERSION;
}; };
const changeLogo = async (file: File) => {
const form = new FormData();
form.append("file", file);
await api.post("/configs/admin/logo", form);
};
export default { export default {
list, list,
getByCategory, getByCategory,
@@ -54,4 +60,5 @@ export default {
finishSetup, finishSetup,
sendTestEmail, sendTestEmail,
isNewReleaseAvailable, isNewReleaseAvailable,
changeLogo,
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share", "name": "pingvin-share",
"version": "0.11.1", "version": "0.12.0",
"scripts": { "scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format", "format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint", "lint": "cd frontend && npm run lint && cd ../backend && npm run lint",