Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c2a62b0ca | ||
|
|
452c635933 | ||
|
|
0455ba1bc1 | ||
|
|
3ad6b03b6b | ||
|
|
91c3525b15 | ||
|
|
8403d7e14d | ||
|
|
8f71fd3435 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -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
2052
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
32
backend/src/config/logo.service.ts
Normal file
32
backend/src/config/logo.service.ts
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
1890
frontend/package-lock.json
generated
1890
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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;
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)`,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" && (
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export function getServerSideProps() {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/admin/config/general",
|
|
||||||
},
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Config = () => {
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Config;
|
|
||||||
@@ -41,7 +41,7 @@ const Admin = () => {
|
|||||||
{
|
{
|
||||||
title: "Configuration",
|
title: "Configuration",
|
||||||
icon: TbSettings,
|
icon: TbSettings,
|
||||||
route: "/admin/config",
|
route: "/admin/config/general",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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%",
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user