Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adf0f8d57e | ||
|
|
447c86f1c9 | ||
|
|
1466240461 | ||
|
|
348852cfa4 | ||
|
|
932496a121 | ||
|
|
0c7b2a8e70 | ||
|
|
1df5c7123e | ||
|
|
2dc0fc9332 | ||
|
|
98c0de78e8 | ||
|
|
5132d177b8 | ||
|
|
e5071cba12 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
!/backend/prisma/.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/backend/src/constants.ts
|
||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
|||||||
|
## [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)
|
### [0.14.1](https://github.com/stonith404/pingvin-share/compare/v0.14.0...v0.14.1) (2023-04-07)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ RUN npm run build && npm prune --production
|
|||||||
# Stage 5: Final image
|
# Stage 5: Final image
|
||||||
FROM node:19-slim AS runner
|
FROM node:19-slim AS runner
|
||||||
ENV NODE_ENV=docker
|
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
|
WORKDIR /opt/app/frontend
|
||||||
COPY --from=frontend-builder /opt/app/public ./public
|
COPY --from=frontend-builder /opt/app/public ./public
|
||||||
@@ -47,4 +47,6 @@ COPY --from=backend-builder /opt/app/package.json ./
|
|||||||
|
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
EXPOSE 3000
|
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
|
CMD cp -rn /tmp/img /opt/app/frontend/public && node frontend/server.js & cd backend && npm run prod
|
||||||
25
README.md
25
README.md
@@ -117,9 +117,30 @@ docker compose up -d
|
|||||||
pm2 restart pingvin-share-frontend
|
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
|
## 🖤 Contribute
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.3.9",
|
"@nestjs/common": "^9.3.9",
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/config": "^2.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.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",
|
||||||
|
|||||||
2
backend/prisma/.env
Normal file
2
backend/prisma/.env
Normal 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"
|
||||||
@@ -4,7 +4,7 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = "file:../data/pingvin-share.db"
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
const configVariables: ConfigVariables = {
|
const configVariables: ConfigVariables = {
|
||||||
internal: {
|
internal: {
|
||||||
jwtSecret: {
|
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() {
|
async function seedConfigVariables() {
|
||||||
for (const [category, configVariablesOfCategory] of Object.entries(
|
for (const [category, configVariablesOfCategory] of Object.entries(
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import * as NodeClam from "clamscan";
|
import * as NodeClam from "clamscan";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { CLAMAV_HOST, CLAMAV_PORT, SHARE_DIRECTORY } from "../constants";
|
||||||
|
|
||||||
const clamscanConfig = {
|
const clamscanConfig = {
|
||||||
clamdscan: {
|
clamdscan: {
|
||||||
host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1",
|
host: CLAMAV_HOST,
|
||||||
port: 3310,
|
port: CLAMAV_PORT,
|
||||||
localFallback: false,
|
localFallback: false,
|
||||||
},
|
},
|
||||||
preference: "clamdscan",
|
preference: "clamdscan",
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClamScanService {
|
export class ClamScanService {
|
||||||
|
private readonly logger = new Logger(ClamScanService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
private prisma: PrismaService
|
private prisma: PrismaService
|
||||||
@@ -23,11 +25,11 @@ export class ClamScanService {
|
|||||||
private ClamScan: Promise<NodeClam | null> = new NodeClam()
|
private ClamScan: Promise<NodeClam | null> = new NodeClam()
|
||||||
.init(clamscanConfig)
|
.init(clamscanConfig)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("ClamAV is active");
|
this.logger.log("ClamAV is active");
|
||||||
return res;
|
return res;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log("ClamAV is not active");
|
this.logger.log("ClamAV is not active");
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,14 +41,14 @@ export class ClamScanService {
|
|||||||
const infectedFiles = [];
|
const infectedFiles = [];
|
||||||
|
|
||||||
const files = fs
|
const files = fs
|
||||||
.readdirSync(`./data/uploads/shares/${shareId}`)
|
.readdirSync(`${SHARE_DIRECTORY}/${shareId}`)
|
||||||
.filter((file) => file != "archive.zip");
|
.filter((file) => file != "archive.zip");
|
||||||
|
|
||||||
for (const fileId of files) {
|
for (const fileId of files) {
|
||||||
const { isInfected } = await clamScan
|
const { isInfected } = await clamScan
|
||||||
.isInfected(`./data/uploads/shares/${shareId}/${fileId}`)
|
.isInfected(`${SHARE_DIRECTORY}/${shareId}/${fileId}`)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log("ClamAV is not active");
|
this.logger.log("ClamAV is not active");
|
||||||
return { isInfected: false };
|
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)`
|
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
backend/src/constants.ts
Normal file
5
backend/src/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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;
|
||||||
@@ -11,6 +11,7 @@ import * as fs from "fs";
|
|||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
import { ConfigService } from "src/config/config.service";
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { SHARE_DIRECTORY } from "../constants";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
@@ -39,7 +40,7 @@ export class FileService {
|
|||||||
let diskFileSize: number;
|
let diskFileSize: number;
|
||||||
try {
|
try {
|
||||||
diskFileSize = fs.statSync(
|
diskFileSize = fs.statSync(
|
||||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`
|
||||||
).size;
|
).size;
|
||||||
} catch {
|
} catch {
|
||||||
diskFileSize = 0;
|
diskFileSize = 0;
|
||||||
@@ -78,18 +79,18 @@ export class FileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
||||||
buffer
|
buffer
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLastChunk = chunk.index == chunk.total - 1;
|
const isLastChunk = chunk.index == chunk.total - 1;
|
||||||
if (isLastChunk) {
|
if (isLastChunk) {
|
||||||
fs.renameSync(
|
fs.renameSync(
|
||||||
`./data/uploads/shares/${shareId}/${file.id}.tmp-chunk`,
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
|
||||||
`./data/uploads/shares/${shareId}/${file.id}`
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
|
||||||
);
|
);
|
||||||
const fileSize = fs.statSync(
|
const fileSize = fs.statSync(
|
||||||
`./data/uploads/shares/${shareId}/${file.id}`
|
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
|
||||||
).size;
|
).size;
|
||||||
await this.prisma.file.create({
|
await this.prisma.file.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -111,9 +112,7 @@ export class FileService {
|
|||||||
|
|
||||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||||
|
|
||||||
const file = fs.createReadStream(
|
const file = fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||||
`./data/uploads/shares/${shareId}/${fileId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metaData: {
|
metaData: {
|
||||||
@@ -126,13 +125,13 @@ export class FileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllFiles(shareId: string) {
|
async deleteAllFiles(shareId: string) {
|
||||||
await fs.promises.rm(`./data/uploads/shares/${shareId}`, {
|
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getZip(shareId: string) {
|
getZip(shareId: string) {
|
||||||
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
|
return fs.createReadStream(`${SHARE_DIRECTORY}/${shareId}/archive.zip`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
||||||
|
import { SHARE_DIRECTORY } from "../constants";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobsService {
|
export class JobsService {
|
||||||
|
private readonly logger = new Logger(JobsService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private reverseShareService: ReverseShareService,
|
private reverseShareService: ReverseShareService,
|
||||||
@@ -34,8 +37,9 @@ export class JobsService {
|
|||||||
await this.fileService.deleteAllFiles(expiredShare.id);
|
await this.fileService.deleteAllFiles(expiredShare.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expiredShares.length > 0)
|
if (expiredShares.length > 0) {
|
||||||
console.log(`job: deleted ${expiredShares.length} expired shares`);
|
this.logger.log(`Deleted ${expiredShares.length} expired shares`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron("0 * * * *")
|
@Cron("0 * * * *")
|
||||||
@@ -50,10 +54,11 @@ export class JobsService {
|
|||||||
await this.reverseShareService.remove(expiredReverseShare.id);
|
await this.reverseShareService.remove(expiredReverseShare.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expiredReverseShares.length > 0)
|
if (expiredReverseShares.length > 0) {
|
||||||
console.log(
|
this.logger.log(
|
||||||
`job: deleted ${expiredReverseShares.length} expired reverse shares`
|
`Deleted ${expiredReverseShares.length} expired reverse shares`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron("0 0 * * *")
|
@Cron("0 0 * * *")
|
||||||
@@ -61,31 +66,31 @@ export class JobsService {
|
|||||||
let filesDeleted = 0;
|
let filesDeleted = 0;
|
||||||
|
|
||||||
const shareDirectories = fs
|
const shareDirectories = fs
|
||||||
.readdirSync("./data/uploads/shares", { withFileTypes: true })
|
.readdirSync(SHARE_DIRECTORY, { withFileTypes: true })
|
||||||
.filter((dirent) => dirent.isDirectory())
|
.filter((dirent) => dirent.isDirectory())
|
||||||
.map((dirent) => dirent.name);
|
.map((dirent) => dirent.name);
|
||||||
|
|
||||||
for (const shareDirectory of shareDirectories) {
|
for (const shareDirectory of shareDirectories) {
|
||||||
const temporaryFiles = fs
|
const temporaryFiles = fs
|
||||||
.readdirSync(`./data/uploads/shares/${shareDirectory}`)
|
.readdirSync(`${SHARE_DIRECTORY}/${shareDirectory}`)
|
||||||
.filter((file) => file.endsWith(".tmp-chunk"));
|
.filter((file) => file.endsWith(".tmp-chunk"));
|
||||||
|
|
||||||
for (const file of temporaryFiles) {
|
for (const file of temporaryFiles) {
|
||||||
const stats = fs.statSync(
|
const stats = fs.statSync(
|
||||||
`./data/uploads/shares/${shareDirectory}/${file}`
|
`${SHARE_DIRECTORY}/${shareDirectory}/${file}`
|
||||||
);
|
);
|
||||||
const isOlderThanOneDay = moment(stats.mtime)
|
const isOlderThanOneDay = moment(stats.mtime)
|
||||||
.add(1, "day")
|
.add(1, "day")
|
||||||
.isBefore(moment());
|
.isBefore(moment());
|
||||||
|
|
||||||
if (isOlderThanOneDay) {
|
if (isOlderThanOneDay) {
|
||||||
fs.rmSync(`./data/uploads/shares/${shareDirectory}/${file}`);
|
fs.rmSync(`${SHARE_DIRECTORY}/${shareDirectory}/${file}`);
|
||||||
filesDeleted++;
|
filesDeleted++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`job: deleted ${filesDeleted} temporary files`);
|
this.logger.log(`Deleted ${filesDeleted} temporary files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron("0 * * * *")
|
@Cron("0 * * * *")
|
||||||
@@ -107,7 +112,8 @@ export class JobsService {
|
|||||||
const deletedTokensCount =
|
const deletedTokensCount =
|
||||||
refreshTokenCount + loginTokenCount + resetPasswordTokenCount;
|
refreshTokenCount + loginTokenCount + resetPasswordTokenCount;
|
||||||
|
|
||||||
if (deletedTokensCount > 0)
|
if (deletedTokensCount > 0) {
|
||||||
console.log(`job: deleted ${deletedTokensCount} expired refresh tokens`);
|
this.logger.log(`Deleted ${deletedTokensCount} expired refresh tokens`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as bodyParser from "body-parser";
|
|||||||
import * as cookieParser from "cookie-parser";
|
import * as cookieParser from "cookie-parser";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
import { DATA_DIRECTORY } from "./constants";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
@@ -16,7 +17,9 @@ async function bootstrap() {
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.set("trust proxy", true);
|
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");
|
app.setGlobalPrefix("api");
|
||||||
|
|
||||||
@@ -30,6 +33,6 @@ async function bootstrap() {
|
|||||||
SwaggerModule.setup("api/swagger", app, document);
|
SwaggerModule.setup("api/swagger", app, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.listen(8080);
|
await app.listen(parseInt(process.env.PORT) || 8080);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { DATABASE_URL } from "../constants";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient {
|
export class PrismaService extends PrismaClient {
|
||||||
@@ -7,7 +8,7 @@ export class PrismaService extends PrismaClient {
|
|||||||
super({
|
super({
|
||||||
datasources: {
|
datasources: {
|
||||||
db: {
|
db: {
|
||||||
url: "file:../data/pingvin-share.db?connection_limit=1",
|
url: DATABASE_URL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Expose, plainToClass } from "class-transformer";
|
import { Expose, plainToClass, Type } from "class-transformer";
|
||||||
import { ShareDTO } from "./share.dto";
|
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()
|
@Expose()
|
||||||
views: number;
|
views: number;
|
||||||
|
|
||||||
@@ -11,6 +17,10 @@ export class MyShareDTO extends ShareDTO {
|
|||||||
@Expose()
|
@Expose()
|
||||||
recipients: string[];
|
recipients: string[];
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@Type(() => OmitType(FileDTO, ["share", "from"] as const))
|
||||||
|
files: Omit<FileDTO, "share" | "from">[];
|
||||||
|
|
||||||
from(partial: Partial<MyShareDTO>) {
|
from(partial: Partial<MyShareDTO>) {
|
||||||
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
|
||||||
}
|
}
|
||||||
@@ -20,4 +30,4 @@ export class MyShareDTO extends ShareDTO {
|
|||||||
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true })
|
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ import { EmailService } from "src/email/email.service";
|
|||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
|
||||||
|
import { SHARE_DIRECTORY } from "../constants";
|
||||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -65,7 +66,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(`./data/uploads/shares/${share.id}`, {
|
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createZip(shareId: string) {
|
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 files = await this.prisma.file.findMany({ where: { shareId } });
|
||||||
const archive = archiver("zip", {
|
const archive = archiver("zip", {
|
||||||
@@ -194,7 +195,7 @@ export class ShareService {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
expiration: "desc",
|
expiration: "desc",
|
||||||
},
|
},
|
||||||
include: { recipients: true },
|
include: { recipients: true, files: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return shares.map((share) => {
|
return shares.map((share) => {
|
||||||
|
|||||||
@@ -19,4 +19,7 @@ module.exports = withPWA({
|
|||||||
output: "standalone", env: {
|
output: "standalone", env: {
|
||||||
VERSION: version,
|
VERSION: version,
|
||||||
},
|
},
|
||||||
|
serverRuntimeConfig: {
|
||||||
|
apiURL: process.env.API_URL ?? 'http://localhost:8080',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.14.1",
|
"version": "0.16.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -116,10 +116,9 @@ const TextPreview = () => {
|
|||||||
const [text, setText] = useState<string | null>(null);
|
const [text, setText] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get(`/shares/${shareId}/files/${fileId}?download=false`).then((res) => {
|
api
|
||||||
console.log(res.data);
|
.get(`/shares/${shareId}/files/${fileId}?download=false`)
|
||||||
setText(res.data);
|
.then((res) => setText(res.data));
|
||||||
});
|
|
||||||
}, [shareId, fileId]);
|
}, [shareId, fileId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ActionIcon, Button, Stack, TextInput } from "@mantine/core";
|
import { Button, Stack } from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import { TbCopy } from "react-icons/tb";
|
import CopyTextField from "../../upload/CopyTextField";
|
||||||
import toast from "../../../utils/toast.util";
|
|
||||||
|
|
||||||
const showCompletedReverseShareModal = (
|
const showCompletedReverseShareModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
@@ -26,28 +24,11 @@ const Body = ({
|
|||||||
link: string;
|
link: string;
|
||||||
getReverseShares: () => void;
|
getReverseShares: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput
|
<CopyTextField link={link} />
|
||||||
readOnly
|
|
||||||
variant="filled"
|
|
||||||
value={link}
|
|
||||||
rightSection={
|
|
||||||
window.isSecureContext && (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.copy(link);
|
|
||||||
toast.success("Your link was copied to the keyboard.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TbCopy />
|
|
||||||
</ActionIcon>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
48
frontend/src/components/upload/CopyTextField.tsx
Normal file
48
frontend/src/components/upload/CopyTextField.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import toast from "../../utils/toast.util";
|
||||||
|
import { ActionIcon, TextInput } from "@mantine/core";
|
||||||
|
import { TbCheck, TbCopy } from "react-icons/tb";
|
||||||
|
import { useClipboard } from "@mantine/hooks";
|
||||||
|
|
||||||
|
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("Your link was copied to the keyboard.");
|
||||||
|
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;
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import { ActionIcon, Button, Stack, Text, TextInput } from "@mantine/core";
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbCopy } from "react-icons/tb";
|
|
||||||
import { Share } from "../../../types/share.type";
|
import { Share } from "../../../types/share.type";
|
||||||
import toast from "../../../utils/toast.util";
|
import CopyTextField from "../CopyTextField";
|
||||||
|
|
||||||
const showCompletedUploadModal = (
|
const showCompletedUploadModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
@@ -23,30 +21,14 @@ const showCompletedUploadModal = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const link = `${appUrl}/share/${share.id}`;
|
const link = `${appUrl}/share/${share.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput
|
<CopyTextField link={link} />
|
||||||
readOnly
|
|
||||||
variant="filled"
|
|
||||||
value={link}
|
|
||||||
rightSection={
|
|
||||||
window.isSecureContext && (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.copy(link);
|
|
||||||
toast.success("Your link was copied to the keyboard.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TbCopy />
|
|
||||||
</ActionIcon>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Text
|
<Text
|
||||||
size="xs"
|
size="xs"
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
// Get config from backend
|
// Get config from backend
|
||||||
const config = await (
|
const config = await (
|
||||||
await fetch("http://localhost:8080/api/configs")
|
await fetch(`${request.nextUrl.origin}/api/configs`)
|
||||||
).json();
|
).json();
|
||||||
|
|
||||||
const getConfig = (key: string) => {
|
const getConfig = (key: string) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import axios from "axios";
|
|||||||
import { getCookie, setCookie } from "cookies-next";
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
import getConfig from "next/config";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Header from "../components/header/Header";
|
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
|
// 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
|
// These will get passed as a page prop to the App component and stored in the contexts
|
||||||
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||||
|
const { apiURL } = getConfig().serverRuntimeConfig;
|
||||||
|
|
||||||
let pageProps: {
|
let pageProps: {
|
||||||
user?: CurrentUser;
|
user?: CurrentUser;
|
||||||
configVariables?: Config[];
|
configVariables?: Config[];
|
||||||
@@ -130,15 +133,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
|||||||
if (ctx.req) {
|
if (ctx.req) {
|
||||||
const cookieHeader = ctx.req.headers.cookie;
|
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 },
|
headers: { cookie: cookieHeader },
|
||||||
})
|
})
|
||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
pageProps.configVariables = (
|
pageProps.configVariables = (await axios(`${apiURL}/api/configs`)).data;
|
||||||
await axios(`http://localhost:8080/api/configs`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
pageProps.route = ctx.req.url;
|
pageProps.route = ctx.req.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
|
MediaQuery,
|
||||||
Space,
|
Space,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
@@ -15,7 +16,7 @@ import { useModals } from "@mantine/modals";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbLink, TbTrash } from "react-icons/tb";
|
import { TbLink, TbTrash, TbInfoCircle } from "react-icons/tb";
|
||||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||||
import CenterLoader from "../../components/core/CenterLoader";
|
import CenterLoader from "../../components/core/CenterLoader";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
@@ -23,6 +24,7 @@ import useConfig from "../../hooks/config.hook";
|
|||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
import { MyShare } from "../../types/share.type";
|
import { MyShare } from "../../types/share.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||||
|
|
||||||
const MyShares = () => {
|
const MyShares = () => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
@@ -60,6 +62,10 @@ const MyShares = () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
|
||||||
|
<th>Description</th>
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
<th>Visitors</th>
|
<th>Visitors</th>
|
||||||
<th>Expires at</th>
|
<th>Expires at</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -69,6 +75,18 @@ const MyShares = () => {
|
|||||||
{shares.map((share) => (
|
{shares.map((share) => (
|
||||||
<tr key={share.id}>
|
<tr key={share.id}>
|
||||||
<td>{share.id}</td>
|
<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>{share.views}</td>
|
||||||
<td>
|
<td>
|
||||||
{moment(share.expiration).unix() === 0
|
{moment(share.expiration).unix() === 0
|
||||||
@@ -77,6 +95,21 @@ const MyShares = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Group position="right">
|
<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
|
<ActionIcon
|
||||||
color="victoria"
|
color="victoria"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import httpProxyMiddleware from "next-http-proxy-middleware";
|
import httpProxyMiddleware from "next-http-proxy-middleware";
|
||||||
|
import getConfig from "next/config";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
@@ -8,11 +9,13 @@ export const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { apiURL } = getConfig().serverRuntimeConfig;
|
||||||
|
|
||||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
return httpProxyMiddleware(req, res, {
|
return httpProxyMiddleware(req, res, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
|
||||||
},
|
},
|
||||||
target: "http://localhost:8080",
|
target: apiURL,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
14
frontend/src/pages/api/health.tsx
Normal file
14
frontend/src/pages/api/health.tsx
Normal 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);
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ export type ShareMetaData = {
|
|||||||
|
|
||||||
export type MyShare = Share & {
|
export type MyShare = Share & {
|
||||||
views: number;
|
views: number;
|
||||||
cratedAt: Date;
|
createdAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MyReverseShare = {
|
export type MyReverseShare = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.14.1",
|
"version": "0.16.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