Compare commits

..

15 Commits

Author SHA1 Message Date
Elias Schneider
7c5ec8d0ea release: 0.16.1 2023-07-10 14:13:58 +02:00
Pierre Bidet
0276294f52 feat: Adding reverse shares' shares a clickable link (#190)
* Add clickable link to reverse share's shares

* Ran format

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-07-10 14:01:55 +02:00
Pierre Bidet
7574eb3191 feat: Adding reverse share ability to copy the link (#191)
* Add clickable link to reverse share's shares

* Ran format

* Adding copy icon to the reverse share list

* Remove console.log

* Ran format

* Ran format in backend

* fix: copy to clipboard spelling

* Open the share in another window

* feat: Adding reverse shares' shares a clickable link (#178)

* Add clickable link to reverse share's shares

* Ran format

* fix: set link default value to random (#181)

* fix: set link default value to random

* fix: add auto EOL and add conventional-changelog package

* Apply suggestions from code review

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* feat: Adding reverse share ability to copy the link (#179)

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-07-10 13:58:29 +02:00
Pierre Bidet
a1ea7c0265 fix: set link default value to random (#192)
* fix: set link default value to random

* fix: add auto EOL and add conventional-changelog package

* feat: Adding reverse shares' shares a clickable link (#178)

* Add clickable link to reverse share's shares

* Ran format

* Apply suggestions from code review

* fix: set link default value to random (#181)

* fix: set link default value to random

* fix: add auto EOL and add conventional-changelog package

* Apply suggestions from code review

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* feat: Adding reverse share ability to copy the link (#179)

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-07-10 13:58:17 +02:00
Elias Schneider
adf0f8d57e release: 0.16.0 2023-07-09 17:15:26 +02:00
Elias Schneider
447c86f1c9 chore: remove backend Dockerfile 2023-06-28 15:45:54 +02:00
pierrbt
1466240461 feat: Adding more informations on My Shares page (table and modal) (#174)
* Adding an information button to the shares and corrected MyShare interface

* Adding other informations and disk usage

* Adding description, disk usage

* Add case if the expiration is never

* Adding file size and better UI

* UI changes to Information Modal

* Adding description to the My Shares page

* Ran format

* Remove string type

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* Remove string type check

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* Remove string type conversion

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* Variable name changes

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* Remove color

Co-authored-by: Elias Schneider <login@eliasschneider.com>

* Requested changes made

* Ran format

* Adding MediaQuery

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-06-26 08:22:15 +02:00
pierrbt
348852cfa4 feat: Adding the possibility of copying the link by clicking text and icons (#171) 2023-06-23 20:07:49 +02:00
Elias Schneider
932496a121 release: 0.15.0 2023-05-09 09:18:31 +02:00
Elias Schneider
0c7b2a8e70 docs: add environment variables to the README 2023-05-09 09:18:02 +02:00
Elias Schneider
1df5c7123e feat: allow to configure clamav with environment variables 2023-05-09 08:45:56 +02:00
Elias Schneider
2dc0fc9332 refactor: improve logging 2023-05-09 08:45:30 +02:00
Elias Schneider
98c0de78e8 feat: add env variables for port, database url and data dir 2023-05-05 11:37:02 +02:00
Elias Schneider
5132d177b8 feat: add healthcheck endpoint 2023-04-27 22:31:06 +02:00
Elias Schneider
e5071cba12 feat: configure ports, db url and api url with env variables 2023-04-25 23:39:57 +02:00
38 changed files with 430 additions and 144 deletions

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ yarn-error.log*
# env file
.env
!/backend/prisma/.env
# vercel
.vercel

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
/backend/src/constants.ts

View File

@@ -1,3 +1,34 @@
### [0.16.1](https://github.com/stonith404/pingvin-share/compare/v0.16.0...v0.16.1) (2023-07-10)
### Features
* Adding reverse share ability to copy the link ([#191](https://github.com/stonith404/pingvin-share/issues/191)) ([7574eb3](https://github.com/stonith404/pingvin-share/commit/7574eb3191f21aadd64f436e9e7c78d3e3973a07)), closes [#178](https://github.com/stonith404/pingvin-share/issues/178) [#181](https://github.com/stonith404/pingvin-share/issues/181)
* Adding reverse shares' shares a clickable link ([#190](https://github.com/stonith404/pingvin-share/issues/190)) ([0276294](https://github.com/stonith404/pingvin-share/commit/0276294f5219a7edcc762bc52391b6720cfd741d))
### Bug Fixes
* set link default value to random ([#192](https://github.com/stonith404/pingvin-share/issues/192)) ([a1ea7c0](https://github.com/stonith404/pingvin-share/commit/a1ea7c026594a54eafd52f764eecbf06e1bb4d4e)), closes [#178](https://github.com/stonith404/pingvin-share/issues/178) [#181](https://github.com/stonith404/pingvin-share/issues/181)
## [0.16.0](https://github.com/stonith404/pingvin-share/compare/v0.15.0...v0.16.0) (2023-07-09)
### Features
* Adding more informations on My Shares page (table and modal) ([#174](https://github.com/stonith404/pingvin-share/issues/174)) ([1466240](https://github.com/stonith404/pingvin-share/commit/14662404614f15bc25384d924d8cb0458ab06cd8))
* Adding the possibility of copying the link by clicking text and icons ([#171](https://github.com/stonith404/pingvin-share/issues/171)) ([348852c](https://github.com/stonith404/pingvin-share/commit/348852cfa4275f5c642669b43697f83c35333044))
## [0.15.0](https://github.com/stonith404/pingvin-share/compare/v0.14.1...v0.15.0) (2023-05-09)
### Features
* add env variables for port, database url and data dir ([98c0de7](https://github.com/stonith404/pingvin-share/commit/98c0de78e8a73e3e5bf0928226cfb8a024b566a1))
* add healthcheck endpoint ([5132d17](https://github.com/stonith404/pingvin-share/commit/5132d177b8ab4e00a7e701e9956222fa2352d42c))
* allow to configure clamav with environment variables ([1df5c71](https://github.com/stonith404/pingvin-share/commit/1df5c7123e4ca8695f4f1b7d49f46cdf147fb920))
* configure ports, db url and api url with env variables ([e5071cb](https://github.com/stonith404/pingvin-share/commit/e5071cba1204093197b72e18d024b484e72e360a))
### [0.14.1](https://github.com/stonith404/pingvin-share/compare/v0.14.0...v0.14.1) (2023-04-07)

View File

@@ -31,7 +31,7 @@ RUN npm run build && npm prune --production
# Stage 5: Final image
FROM node:19-slim AS runner
ENV NODE_ENV=docker
RUN apt-get update && apt-get install -y openssl
RUN apt-get update && apt-get install -y curl openssl
WORKDIR /opt/app/frontend
COPY --from=frontend-builder /opt/app/public ./public
@@ -47,4 +47,6 @@ COPY --from=backend-builder /opt/app/package.json ./
WORKDIR /opt/app
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod

View File

@@ -117,9 +117,30 @@ docker compose up -d
pm2 restart pingvin-share-frontend
```
### Custom branding
### Configuration
You can change the name and the logo of the app by visiting the admin configuration page.
You can customize Pingvin Share by going to the configuration page in your admin dashboard.
#### Environment variables
For installation specific configuration, you can use environment variables. The following variables are available:
##### Backend
| Variable | Default Value | Description |
| ---------------- | -------------------------------------------------- | -------------------------------------- |
| `PORT` | `8080` | The port on which the backend listens. |
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | The URL of the SQLite database. |
| `DATA_DIRECTORY` | `./data` | The directory where data is stored. |
| `CLAMAV_HOST` | `127.0.0.1` | The IP address of the ClamAV server. |
| `CLAMAV_PORT` | `3310` | The port number of the ClamAV server. |
##### Frontend
| Variable | Default Value | Description |
| --------- | ----------------------- | ---------------------------------------- |
| `PORT` | `3000` | The port on which the frontend listens. |
| `API_URL` | `http://localhost:8080` | The URL of the backend for the frontend. |
## 🖤 Contribute

View File

@@ -1,22 +0,0 @@
FROM node:18 AS deps
WORKDIR /opt/app
COPY package.json package-lock.json ./
COPY prisma ./prisma
RUN npm ci
RUN npx prisma generate
FROM node:18 As build
WORKDIR /opt/app
COPY . .
COPY --from=deps /opt/app/node_modules ./node_modules
RUN npm run build
FROM node:18 As runner
WORKDIR /opt/app
COPY --from=build /opt/app/node_modules ./node_modules
COPY --from=build /opt/app/dist ./dist
COPY --from=build /opt/app/prisma ./prisma
COPY --from=deps /opt/app/package.json ./
CMD npm run prod

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-backend",
"version": "0.14.1",
"version": "0.16.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-backend",
"version": "0.14.1",
"version": "0.16.1",
"dependencies": {
"@nestjs/common": "^9.3.9",
"@nestjs/config": "^2.3.1",

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-backend",
"version": "0.14.1",
"version": "0.16.1",
"scripts": {
"build": "nest build",
"dev": "cross-env NODE_ENV=development nest start --watch",
"prod": "prisma migrate deploy && prisma db seed && node dist/src/main",
"lint": "eslint 'src/**/*.ts'",
"format": "prettier --write 'src/**/*.ts'",
"format": "prettier --end-of-line=auto --write 'src/**/*.ts'",
"test:system": "prisma migrate reset -f && nest start & wait-on http://localhost:8080/api/configs && newman run ./test/newman-system-tests.json"
},
"prisma": {

2
backend/prisma/.env Normal file
View File

@@ -0,0 +1,2 @@
#This file is only used to set a default value for the database url
DATABASE_URL="file:../data/pingvin-share.db"

View File

@@ -4,7 +4,7 @@ generator client {
datasource db {
provider = "sqlite"
url = "file:../data/pingvin-share.db"
url = env("DATABASE_URL")
}
model User {

View File

@@ -1,6 +1,5 @@
import { Prisma, PrismaClient } from "@prisma/client";
import * as crypto from "crypto";
const configVariables: ConfigVariables = {
internal: {
jwtSecret: {
@@ -162,7 +161,15 @@ type ConfigVariables = {
};
};
const prisma = new PrismaClient();
const prisma = new PrismaClient({
datasources: {
db: {
url:
process.env.DATABASE_URL ||
"file:../data/pingvin-share.db?connection_limit=1",
},
},
});
async function seedConfigVariables() {
for (const [category, configVariablesOfCategory] of Object.entries(

View File

@@ -1,20 +1,22 @@
import { Injectable } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import * as NodeClam from "clamscan";
import * as fs from "fs";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
import { CLAMAV_HOST, CLAMAV_PORT, SHARE_DIRECTORY } from "../constants";
const clamscanConfig = {
clamdscan: {
host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1",
port: 3310,
host: CLAMAV_HOST,
port: CLAMAV_PORT,
localFallback: false,
},
preference: "clamdscan",
};
@Injectable()
export class ClamScanService {
private readonly logger = new Logger(ClamScanService.name);
constructor(
private fileService: FileService,
private prisma: PrismaService
@@ -23,11 +25,11 @@ export class ClamScanService {
private ClamScan: Promise<NodeClam | null> = new NodeClam()
.init(clamscanConfig)
.then((res) => {
console.log("ClamAV is active");
this.logger.log("ClamAV is active");
return res;
})
.catch(() => {
console.log("ClamAV is not active");
this.logger.log("ClamAV is not active");
return null;
});
@@ -39,14 +41,14 @@ export class ClamScanService {
const infectedFiles = [];
const files = fs
.readdirSync(`./data/uploads/shares/${shareId}`)
.readdirSync(`${SHARE_DIRECTORY}/${shareId}`)
.filter((file) => file != "archive.zip");
for (const fileId of files) {
const { isInfected } = await clamScan
.isInfected(`./data/uploads/shares/${shareId}/${fileId}`)
.isInfected(`${SHARE_DIRECTORY}/${shareId}/${fileId}`)
.catch(() => {
console.log("ClamAV is not active");
this.logger.log("ClamAV is not active");
return { isInfected: false };
});
@@ -78,7 +80,7 @@ export class ClamScanService {
},
});
console.log(
this.logger.warn(
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
);
}

9
backend/src/constants.ts Normal file
View File

@@ -0,0 +1,9 @@
export const DATA_DIRECTORY = process.env.DATA_DIRECTORY || "./data";
export const SHARE_DIRECTORY = `${DATA_DIRECTORY}/uploads/shares`;
export const DATABASE_URL =
process.env.DATABASE_URL ||
"file:../data/pingvin-share.db?connection_limit=1";
export const CLAMAV_HOST =
process.env.CLAMAV_HOST ||
(process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1");
export const CLAMAV_PORT = parseInt(process.env.CLAMAV_PORT) || 3310;

View File

@@ -11,6 +11,7 @@ import * as fs from "fs";
import * as mime from "mime-types";
import { ConfigService } from "src/config/config.service";
import { PrismaService } from "src/prisma/prisma.service";
import { SHARE_DIRECTORY } from "../constants";
@Injectable()
export class FileService {
@@ -39,7 +40,7 @@ export class FileService {
let diskFileSize: number;
try {
diskFileSize = fs.statSync(
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`
).size;
} catch {
diskFileSize = 0;
@@ -78,18 +79,18 @@ export class FileService {
}
fs.appendFileSync(
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
buffer
);
const isLastChunk = chunk.index == chunk.total - 1;
if (isLastChunk) {
fs.renameSync(
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
`./data/uploads/shares/${shareId}/${file.id}`
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
);
const fileSize = fs.statSync(
`./data/uploads/shares/${shareId}/${file.id}`
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
).size;
await this.prisma.file.create({
data: {
@@ -111,9 +112,7 @@ export class FileService {
if (!fileMetaData) throw new NotFoundException("File not found");
const file = fs.createReadStream(
`./data/uploads/shares/${shareId}/${fileId}`
);
const file = fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
return {
metaData: {
@@ -126,13 +125,13 @@ export class FileService {
}
async deleteAllFiles(shareId: string) {
await fs.promises.rm(`./data/uploads/shares/${shareId}`, {
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
recursive: true,
force: true,
});
}
getZip(shareId: string) {
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
return fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/archive.zip`);
}
}

View File

@@ -1,13 +1,16 @@
import { Injectable } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import * as fs from "fs";
import * as moment from "moment";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
import { SHARE_DIRECTORY } from "../constants";
@Injectable()
export class JobsService {
private readonly logger = new Logger(JobsService.name);
constructor(
private prisma: PrismaService,
private reverseShareService: ReverseShareService,
@@ -34,8 +37,9 @@ export class JobsService {
await this.fileService.deleteAllFiles(expiredShare.id);
}
if (expiredShares.length > 0)
console.log(`job: deleted ${expiredShares.length} expired shares`);
if (expiredShares.length > 0) {
this.logger.log(`Deleted ${expiredShares.length} expired shares`);
}
}
@Cron("0 * * * *")
@@ -50,10 +54,11 @@ export class JobsService {
await this.reverseShareService.remove(expiredReverseShare.id);
}
if (expiredReverseShares.length > 0)
console.log(
`job: deleted ${expiredReverseShares.length} expired reverse shares`
if (expiredReverseShares.length > 0) {
this.logger.log(
`Deleted ${expiredReverseShares.length} expired reverse shares`
);
}
}
@Cron("0 0 * * *")
@@ -61,31 +66,31 @@ export class JobsService {
let filesDeleted = 0;
const shareDirectories = fs
.readdirSync("./data/uploads/shares", { withFileTypes: true })
.readdirSync(SHARE_DIRECTORY, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
for (const shareDirectory of shareDirectories) {
const temporaryFiles = fs
.readdirSync(`./data/uploads/shares/${shareDirectory}`)
.readdirSync(`${SHARE_DIRECTORY}/${shareDirectory}`)
.filter((file) => file.endsWith(".tmp-chunk"));
for (const file of temporaryFiles) {
const stats = fs.statSync(
`./data/uploads/shares/${shareDirectory}/${file}`
`${SHARE_DIRECTORY}/${shareDirectory}/${file}`
);
const isOlderThanOneDay = moment(stats.mtime)
.add(1, "day")
.isBefore(moment());
if (isOlderThanOneDay) {
fs.rmSync(`./data/uploads/shares/${shareDirectory}/${file}`);
fs.rmSync(`${SHARE_DIRECTORY}/${shareDirectory}/${file}`);
filesDeleted++;
}
}
}
console.log(`job: deleted ${filesDeleted} temporary files`);
this.logger.log(`Deleted ${filesDeleted} temporary files`);
}
@Cron("0 * * * *")
@@ -107,7 +112,8 @@ export class JobsService {
const deletedTokensCount =
refreshTokenCount + loginTokenCount + resetPasswordTokenCount;
if (deletedTokensCount > 0)
console.log(`job: deleted ${deletedTokensCount} expired refresh tokens`);
if (deletedTokensCount > 0) {
this.logger.log(`Deleted ${deletedTokensCount} expired refresh tokens`);
}
}
}

View File

@@ -6,6 +6,7 @@ import * as bodyParser from "body-parser";
import * as cookieParser from "cookie-parser";
import * as fs from "fs";
import { AppModule } from "./app.module";
import { DATA_DIRECTORY } from "./constants";
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
@@ -16,7 +17,9 @@ async function bootstrap() {
app.use(cookieParser());
app.set("trust proxy", true);
await fs.promises.mkdir("./data/uploads/_temp", { recursive: true });
await fs.promises.mkdir(`${DATA_DIRECTORY}/uploads/_temp`, {
recursive: true,
});
app.setGlobalPrefix("api");
@@ -30,6 +33,6 @@ async function bootstrap() {
SwaggerModule.setup("api/swagger", app, document);
}
await app.listen(8080);
await app.listen(parseInt(process.env.PORT) || 8080);
}
bootstrap();

View File

@@ -1,5 +1,6 @@
import { Injectable } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
import { DATABASE_URL } from "../constants";
@Injectable()
export class PrismaService extends PrismaClient {
@@ -7,7 +8,7 @@ export class PrismaService extends PrismaClient {
super({
datasources: {
db: {
url: "file:../data/pingvin-share.db?connection_limit=1",
url: DATABASE_URL,
},
},
});

View File

@@ -10,6 +10,9 @@ export class ReverseShareDTO {
@Expose()
shareExpiration: Date;
@Expose()
token: string;
from(partial: Partial<ReverseShareDTO>) {
return plainToClass(ReverseShareDTO, partial, {
excludeExtraneousValues: true,

View File

@@ -1,7 +1,13 @@
import { Expose, plainToClass } from "class-transformer";
import { Expose, plainToClass, Type } from "class-transformer";
import { ShareDTO } from "./share.dto";
import { FileDTO } from "../../file/dto/file.dto";
import { OmitType } from "@nestjs/swagger";
export class MyShareDTO extends ShareDTO {
export class MyShareDTO extends OmitType(ShareDTO, [
"files",
"from",
"fromList",
] as const) {
@Expose()
views: number;
@@ -11,6 +17,10 @@ export class MyShareDTO extends ShareDTO {
@Expose()
recipients: string[];
@Expose()
@Type(() => OmitType(FileDTO, ["share", "from"] as const))
files: Omit<FileDTO, "share" | "from">[];
from(partial: Partial<MyShareDTO>) {
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
}

View File

@@ -16,6 +16,7 @@ import { EmailService } from "src/email/email.service";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
import { SHARE_DIRECTORY } from "../constants";
import { CreateShareDTO } from "./dto/createShare.dto";
@Injectable()
@@ -65,7 +66,7 @@ export class ShareService {
}
}
fs.mkdirSync(`./data/uploads/shares/${share.id}`, {
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {
recursive: true,
});
@@ -99,7 +100,7 @@ export class ShareService {
}
async createZip(shareId: string) {
const path = `./data/uploads/shares/${shareId}`;
const path = `${SHARE_DIRECTORY}/${shareId}`;
const files = await this.prisma.file.findMany({ where: { shareId } });
const archive = archiver("zip", {
@@ -194,7 +195,7 @@ export class ShareService {
orderBy: {
expiration: "desc",
},
include: { recipients: true },
include: { recipients: true, files: true },
});
return shares.map((share) => {

View File

@@ -19,4 +19,7 @@ module.exports = withPWA({
output: "standalone", env: {
VERSION: version,
},
serverRuntimeConfig: {
apiURL: process.env.API_URL ?? 'http://localhost:8080',
},
});

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-frontend",
"version": "0.14.1",
"version": "0.16.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-frontend",
"version": "0.14.1",
"version": "0.16.1",
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-frontend",
"version": "0.14.1",
"version": "0.16.1",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write \"src/**/*.ts*\""
"format": "prettier --end-of-line=auto --write \"src/**/*.ts*\""
},
"dependencies": {
"@emotion/react": "^11.10.6",

View File

@@ -0,0 +1,20 @@
import { Stack, TextInput } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
const showReverseShareLinkModal = (
modals: ModalsContextProps,
reverseShareToken: string,
appUrl: string
) => {
const link = `${appUrl}/upload/${reverseShareToken}`;
return modals.openModal({
title: "Reverse share link",
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />
</Stack>
),
});
};
export default showReverseShareLinkModal;

View File

@@ -0,0 +1,85 @@
import { Text, Divider, Progress, Stack, Group, Flex } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { MyShare } from "../../types/share.type";
import moment from "moment";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import CopyTextField from "../upload/CopyTextField";
import { FileMetaData } from "../../types/File.type";
const showShareInformationsModal = (
modals: ModalsContextProps,
share: MyShare,
appUrl: string,
maxShareSize: number
) => {
const link = `${appUrl}/share/${share.id}`;
let shareSize: number = 0;
for (let file of share.files as FileMetaData[])
shareSize += parseInt(file.size);
const formattedShareSize = byteToHumanSizeString(shareSize);
const formattedMaxShareSize = byteToHumanSizeString(maxShareSize);
const shareSizeProgress = (shareSize / maxShareSize) * 100;
const formattedCreatedAt = moment(share.createdAt).format("LLL");
const formattedExpiration =
moment(share.expiration).unix() === 0
? "Never"
: moment(share.expiration).format("LLL");
return modals.openModal({
title: "Share informations",
children: (
<Stack align="stretch" spacing="md">
<Text size="sm" color="lightgray">
<b>ID:</b> {share.id}
</Text>
<Text size="sm" color="lightgray">
<b>Description:</b> {share.description || "No description"}
</Text>
<Text size="sm" color="lightgray">
<b>Created at:</b> {formattedCreatedAt}
</Text>
<Text size="sm" color="lightgray">
<b>Expires at:</b> {formattedExpiration}
</Text>
<Divider />
<CopyTextField link={link} />
<Divider />
<Text size="sm" color="lightgray">
<b>Size:</b> {formattedShareSize} / {formattedMaxShareSize} (
{shareSizeProgress.toFixed(1)}%)
</Text>
<Flex align="center" justify="center">
{shareSize / maxShareSize < 0.1 && (
<Text size="xs" color="lightgray" style={{ marginRight: "4px" }}>
{formattedShareSize}
</Text>
)}
<Progress
value={shareSizeProgress}
label={shareSize / maxShareSize >= 0.1 ? formattedShareSize : ""}
style={{ width: shareSize / maxShareSize < 0.1 ? "70%" : "80%" }}
size="xl"
radius="xl"
/>
<Text size="xs" color="lightgray" style={{ marginLeft: "4px" }}>
{formattedMaxShareSize}
</Text>
</Flex>
</Stack>
),
});
};
export default showShareInformationsModal;

View File

@@ -116,10 +116,9 @@ const TextPreview = () => {
const [text, setText] = useState<string | null>(null);
useEffect(() => {
api.get(`/shares/${shareId}/files/${fileId}?download=false`).then((res) => {
console.log(res.data);
setText(res.data);
});
api
.get(`/shares/${shareId}/files/${fileId}?download=false`)
.then((res) => setText(res.data));
}, [shareId, fileId]);
return (

View File

@@ -1,9 +1,7 @@
import { ActionIcon, Button, Stack, TextInput } from "@mantine/core";
import { useClipboard } from "@mantine/hooks";
import { Button, Stack } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { TbCopy } from "react-icons/tb";
import toast from "../../../utils/toast.util";
import CopyTextField from "../../upload/CopyTextField";
const showCompletedReverseShareModal = (
modals: ModalsContextProps,
@@ -26,28 +24,11 @@ const Body = ({
link: string;
getReverseShares: () => void;
}) => {
const clipboard = useClipboard({ timeout: 500 });
const modals = useModals();
return (
<Stack align="stretch">
<TextInput
readOnly
variant="filled"
value={link}
rightSection={
window.isSecureContext && (
<ActionIcon
onClick={() => {
clipboard.copy(link);
toast.success("Your link was copied to the keyboard.");
}}
>
<TbCopy />
</ActionIcon>
)
}
/>
<CopyTextField link={link} />
<Button
onClick={() => {

View File

@@ -0,0 +1,48 @@
import { ActionIcon, TextInput } from "@mantine/core";
import { useClipboard } from "@mantine/hooks";
import { useRef, useState } from "react";
import { TbCheck, TbCopy } from "react-icons/tb";
import toast from "../../utils/toast.util";
function CopyTextField(props: { link: string }) {
const clipboard = useClipboard({ timeout: 500 });
const [checkState, setCheckState] = useState(false);
const [textClicked, setTextClicked] = useState(false);
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
undefined
);
const copyLink = () => {
clipboard.copy(props.link);
toast.success("The link was copied to your clipboard.");
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setCheckState(false);
}, 1500);
setCheckState(true);
};
return (
<TextInput
readOnly
label="Link"
variant="filled"
value={props.link}
onClick={() => {
if (!textClicked) {
copyLink();
setTextClicked(true);
}
}}
rightSection={
window.isSecureContext && (
<ActionIcon onClick={copyLink}>
{checkState ? <TbCheck /> : <TbCopy />}
</ActionIcon>
)
}
/>
);
}
export default CopyTextField;

View File

@@ -1,12 +1,10 @@
import { ActionIcon, Button, Stack, Text, TextInput } from "@mantine/core";
import { useClipboard } from "@mantine/hooks";
import { Button, Stack, Text } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { useRouter } from "next/router";
import { TbCopy } from "react-icons/tb";
import { Share } from "../../../types/share.type";
import toast from "../../../utils/toast.util";
import CopyTextField from "../CopyTextField";
const showCompletedUploadModal = (
modals: ModalsContextProps,
@@ -23,30 +21,14 @@ const showCompletedUploadModal = (
};
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
const clipboard = useClipboard({ timeout: 500 });
const modals = useModals();
const router = useRouter();
const link = `${appUrl}/share/${share.id}`;
return (
<Stack align="stretch">
<TextInput
readOnly
variant="filled"
value={link}
rightSection={
window.isSecureContext && (
<ActionIcon
onClick={() => {
clipboard.copy(link);
toast.success("Your link was copied to the keyboard.");
}}
>
<TbCopy />
</ActionIcon>
)
}
/>
<CopyTextField link={link} />
<Text
size="xs"
sx={(theme) => ({

View File

@@ -62,6 +62,10 @@ const CreateUploadModalBody = ({
}) => {
const modals = useModals();
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
.substr(10, 7);
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
const validationSchema = yup.object().shape({
@@ -78,7 +82,7 @@ const CreateUploadModalBody = ({
});
const form = useForm({
initialValues: {
link: "",
link: generatedLink,
recipients: [] as string[],
password: undefined,
maxViews: undefined,

View File

@@ -22,7 +22,7 @@ export async function middleware(request: NextRequest) {
// Get config from backend
const config = await (
await fetch("http://localhost:8080/api/configs")
await fetch(`${request.nextUrl.origin}/api/configs`)
).json();
const getConfig = (key: string) => {

View File

@@ -11,6 +11,7 @@ import axios from "axios";
import { getCookie, setCookie } from "cookies-next";
import { GetServerSidePropsContext } from "next";
import type { AppProps } from "next/app";
import getConfig from "next/config";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Header from "../components/header/Header";
@@ -117,6 +118,8 @@ function App({ Component, pageProps }: AppProps) {
// Fetch user and config variables on server side when the first request is made
// These will get passed as a page prop to the App component and stored in the contexts
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
const { apiURL } = getConfig().serverRuntimeConfig;
let pageProps: {
user?: CurrentUser;
configVariables?: Config[];
@@ -130,15 +133,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
if (ctx.req) {
const cookieHeader = ctx.req.headers.cookie;
pageProps.user = await axios(`http://localhost:8080/api/users/me`, {
pageProps.user = await axios(`${apiURL}/api/users/me`, {
headers: { cookie: cookieHeader },
})
.then((res) => res.data)
.catch(() => null);
pageProps.configVariables = (
await axios(`http://localhost:8080/api/configs`)
).data;
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
pageProps.route = ctx.req.url;
}

View File

@@ -1,6 +1,7 @@
import {
Accordion,
ActionIcon,
Anchor,
Box,
Button,
Center,
@@ -16,9 +17,10 @@ import { useModals } from "@mantine/modals";
import moment from "moment";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
import Meta from "../../components/Meta";
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import Meta from "../../components/Meta";
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
import useConfig from "../../hooks/config.hook";
import shareService from "../../services/share.service";
@@ -34,6 +36,8 @@ const MyShares = () => {
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
const appUrl = config.get("general.appUrl");
const getReverseShares = () => {
shareService
.getMyReverseShares()
@@ -119,9 +123,14 @@ const MyShares = () => {
<Accordion.Panel>
{reverseShare.shares.map((share) => (
<Group key={share.id} mb={4}>
<Text maw={120} truncate>
{share.id}
</Text>
<Anchor
href={`${appUrl}/share/${share.id}`}
target="_blank"
>
<Text maw={120} truncate>
{share.id}
</Text>
</Anchor>
<ActionIcon
color="victoria"
variant="light"
@@ -129,9 +138,7 @@ const MyShares = () => {
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get(
"general.appUrl"
)}/share/${share.id}`
`${appUrl}/share/${share.id}`
);
toast.success(
"The share link was copied to the keyboard."
@@ -165,6 +172,31 @@ const MyShares = () => {
</td>
<td>
<Group position="right">
<ActionIcon
color="victoria"
variant="light"
size={25}
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("general.appUrl")}/upload/${
reverseShare.token
}`
);
toast.success(
"The link was copied to your clipboard."
);
} else {
showReverseShareLinkModal(
modals,
reverseShare.token,
config.get("general.appUrl")
);
}
}}
>
<TbLink />
</ActionIcon>
<ActionIcon
color="red"
variant="light"

View File

@@ -4,6 +4,7 @@ import {
Button,
Center,
Group,
MediaQuery,
Space,
Stack,
Table,
@@ -15,10 +16,11 @@ import { useModals } from "@mantine/modals";
import moment from "moment";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb";
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
import Meta from "../../components/Meta";
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import shareService from "../../services/share.service";
import { MyShare } from "../../types/share.type";
@@ -60,6 +62,10 @@ const MyShares = () => {
<thead>
<tr>
<th>Name</th>
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
<th>Description</th>
</MediaQuery>
<th>Visitors</th>
<th>Expires at</th>
<th></th>
@@ -69,6 +75,18 @@ const MyShares = () => {
{shares.map((share) => (
<tr key={share.id}>
<td>{share.id}</td>
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
<td
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: "300px",
}}
>
{share.description || ""}
</td>
</MediaQuery>
<td>{share.views}</td>
<td>
{moment(share.expiration).unix() === 0
@@ -77,6 +95,21 @@ const MyShares = () => {
</td>
<td>
<Group position="right">
<ActionIcon
color="blue"
variant="light"
size={25}
onClick={() => {
showShareInformationsModal(
modals,
share,
config.get("general.appUrl"),
parseInt(config.get("share.maxSize"))
);
}}
>
<TbInfoCircle />
</ActionIcon>
<ActionIcon
color="victoria"
variant="light"
@@ -89,7 +122,7 @@ const MyShares = () => {
}`
);
toast.success(
"Your link was copied to the keyboard."
"The link was copied to your clipboard."
);
} else {
showShareLinkModal(

View File

@@ -1,5 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
import httpProxyMiddleware from "next-http-proxy-middleware";
import getConfig from "next/config";
export const config = {
api: {
@@ -8,11 +9,13 @@ export const config = {
},
};
const { apiURL } = getConfig().serverRuntimeConfig;
export default (req: NextApiRequest, res: NextApiResponse) => {
return httpProxyMiddleware(req, res, {
headers: {
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
},
target: "http://localhost:8080",
target: apiURL,
});
};

View File

@@ -0,0 +1,14 @@
import axios from "axios";
import { NextApiRequest, NextApiResponse } from "next";
import getConfig from "next/config";
const { apiURL } = getConfig().serverRuntimeConfig;
export default async (req: NextApiRequest, res: NextApiResponse) => {
const apiStatus = await axios
.get(`${apiURL}/api/configs`)
.then(() => "OK")
.catch(() => "ERROR");
res.status(apiStatus == "OK" ? 200 : 500).send(apiStatus);
};

View File

@@ -24,7 +24,7 @@ export type ShareMetaData = {
export type MyShare = Share & {
views: number;
cratedAt: Date;
createdAt: Date;
};
export type MyReverseShare = {
@@ -32,6 +32,7 @@ export type MyReverseShare = {
maxShareSize: string;
shareExpiration: Date;
remainingUses: number;
token: string;
shares: MyShare[];
};

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share",
"version": "0.14.1",
"version": "0.16.1",
"scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
@@ -8,5 +8,8 @@
"release:patch": "cd backend && npm version patch --commit-hooks false && cd ../frontend && npm version patch --commit-hooks false && cd .. && git add . && npm version patch --force -m 'release: %s' && git push && git push --tags",
"release:minor": "cd backend && npm version minor --commit-hooks false && cd ../frontend && npm version minor --commit-hooks false && cd .. && git add . && npm version minor --force -m 'release: %s' && git push && git push --tags",
"deploy:dev": "docker buildx build --push --tag stonith404/pingvin-share:development --platform linux/amd64,linux/arm64 ."
},
"devDependencies": {
"conventional-changelog-cli": "^3.0.0"
}
}