Compare commits
31 Commits
v0.14.0
...
link-copy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36fa76563e | ||
|
|
f96ac5e4ba | ||
|
|
447c86f1c9 | ||
|
|
1466240461 | ||
|
|
bbbf10d233 | ||
|
|
348852cfa4 | ||
|
|
e9d1a9abb6 | ||
|
|
8fdba0ca7c | ||
|
|
e5718700bc | ||
|
|
e40a0c844c | ||
|
|
e647746c93 | ||
|
|
9be77826e9 | ||
|
|
a9bb05c4da | ||
|
|
e1a9f2a27c | ||
|
|
ba62c13cfa | ||
|
|
3de744d5e9 | ||
|
|
61608cfe2d | ||
|
|
db755ef300 | ||
|
|
932496a121 | ||
|
|
0c7b2a8e70 | ||
|
|
1df5c7123e | ||
|
|
2dc0fc9332 | ||
|
|
98c0de78e8 | ||
|
|
5132d177b8 | ||
|
|
e5071cba12 | ||
|
|
b33c1d7f4b | ||
|
|
39a74510c1 | ||
|
|
b7db9b9b40 | ||
|
|
2ca0092b71 | ||
|
|
b4bf43910e | ||
|
|
90aa919694 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ yarn-error.log*
|
||||
|
||||
# env file
|
||||
.env
|
||||
!/backend/prisma/.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
/backend/src/constants.ts
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,3 +1,20 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* boolean config variables can't be set to false ([39a7451](https://github.com/stonith404/pingvin-share/commit/39a74510c1f00466acaead39f7bee003b3db60d7))
|
||||
|
||||
## [0.14.0](https://github.com/stonith404/pingvin-share/compare/v0.13.1...v0.14.0) (2023-04-01)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
*Read this in another language: [Spanish](CONTRIBUTING.es.md), [English](CONTRIBUTING.md)*
|
||||
_Read this in another language: [Spanish](/docs/CONTRIBUTING.es.md), [English](/CONTRIBUTING.md), [Simplified Chinese](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
28
README.md
28
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
|
||||
*Read this in another language: [Spanish](README.es.md), [English](README.md)*
|
||||
_Read this in another language: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
@@ -99,6 +99,7 @@ docker compose up -d
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. Repeat the steps from the [installation guide](#stand-alone-installation) except the `git clone` step.
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
@@ -116,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
|
||||
|
||||
|
||||
@@ -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",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^9.3.9",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"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 {
|
||||
provider = "sqlite"
|
||||
url = "file:../data/pingvin-share.db"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class ConfigService {
|
||||
if (!configVariable || configVariable.locked)
|
||||
throw new NotFoundException("Config variable not found");
|
||||
|
||||
if (value == "") {
|
||||
if (value === "") {
|
||||
value = null;
|
||||
} else if (
|
||||
typeof value != configVariable.type &&
|
||||
@@ -100,7 +100,7 @@ export class ConfigService {
|
||||
name: key.split(".")[1],
|
||||
},
|
||||
},
|
||||
data: { value: value ? value.toString() : null },
|
||||
data: { value: value === null ? null : value.toString() },
|
||||
});
|
||||
|
||||
this.configVariables = await this.prisma.config.findMany();
|
||||
|
||||
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 { 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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,42 +54,43 @@ 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 * * *")
|
||||
deleteTemporaryFiles() {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
*Leer esto en otro idioma: [Inglés](CONTRIBUTING.md), [Español](CONTRIBUTING.es.md)*
|
||||
_Leer esto en otro idioma: [Inglés](/CONTRIBUTING.md), [Español](/docs/CONTRIBUTING.es.md), [Chino Simplificado](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# Contribuyendo
|
||||
|
||||
¡Nos ❤️ encantaría que contribuyas a Pingvin Share y nos ayudes a hacerlo mejor! Todas las contribuciones son bienvenidas, incluyendo problemas, sugerencias, *pull requests* y más.
|
||||
¡Nos ❤️ encantaría que contribuyas a Pingvin Share y nos ayudes a hacerlo mejor! Todas las contribuciones son bienvenidas, incluyendo problemas, sugerencias, _pull requests_ y más.
|
||||
|
||||
## Para comenzar
|
||||
|
||||
97
docs/CONTRIBUTING.zh-cn.md
Normal file
97
docs/CONTRIBUTING.zh-cn.md
Normal file
@@ -0,0 +1,97 @@
|
||||
_选择合适的语言阅读: [西班牙语](/docs/CONTRIBUTING.es.md), [英语](/CONTRIBUTING.md), [简体中文](/docs/CONTRIBUTING.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
# 提交贡献
|
||||
|
||||
我们非常感谢你 ❤️ 为 Pingvin Share 提交贡献使其变得更棒! 欢迎任何形式的贡献,包括 issues, 建议, PRs 和其他形式
|
||||
|
||||
## 小小的开始
|
||||
|
||||
你找到了一个 bug,有新特性建议或者其他提议,请在 GitHub 建立一个 issue 以便我和你联络 😊
|
||||
|
||||
## 提交一个 Pull Request
|
||||
|
||||
在你提交 PR 前请确保
|
||||
|
||||
- PR 的名字遵守 [Conventional Commits specification](https://www.conventionalcommits.org):
|
||||
|
||||
`<type>[optional scope]: <description>`
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
feat(share): add password protection
|
||||
```
|
||||
|
||||
`TYPE` 可以是:
|
||||
|
||||
- **feat** - 这是一个新特性 feature
|
||||
- **doc** - 仅仅改变了文档部分 documentation
|
||||
- **fix** - 修复了一个 bug
|
||||
- **refactor** - 更新了代码,但是并非出于增加新特性 feature 或修复 bug 的目的
|
||||
|
||||
- 请在 PR 中附详细的解释说明
|
||||
- 使用 `npm run format` 格式化你的代码
|
||||
|
||||
<details>
|
||||
<summary>不知道怎么发起一个 PR? 点开了解怎么发起一个 PR </summary>
|
||||
|
||||
1. 点击 Pingvin Share 仓库的 `Fork` 按钮,复制一份你的仓库
|
||||
|
||||
2. 通过 `git clone` 将你的仓库克隆到本地
|
||||
|
||||
```
|
||||
$ git clone https://github.com/[你的用户名]/pingvin-share
|
||||
```
|
||||
|
||||
3. 进行你的修改 - 提交 commit 你的修改 - 重复直到完成
|
||||
|
||||
4. 将你的修改提交到 GitHub
|
||||
|
||||
```
|
||||
$ git push origin [你的新分支的名字]
|
||||
```
|
||||
|
||||
5. 提交你的代码以便代码审查
|
||||
|
||||
如果你进入你 fork 的 Github 仓库,你会看到一个 `Compare & pull request` 按钮,点击该按钮
|
||||
|
||||
6. 发起一个 PR
|
||||
7. 点击 `Create pull request` 来提交你的 PR
|
||||
8. 等待代码审查,通过或以某些原因拒绝
|
||||
|
||||
</details>
|
||||
|
||||
## 配置开发项目
|
||||
|
||||
Pingvin Share 包括前端和后端部分
|
||||
|
||||
### 后端
|
||||
|
||||
后端使用 [Nest.js](https://nestjs.com) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 打开 `backend` 文件夹
|
||||
2. 使用 `npm install` 安装依赖
|
||||
3. 通过 `npx prisma db push` 配置数据库结构
|
||||
4. 通过 `npx prisma db seed` 初始化数据库数据
|
||||
5. 通过 `npm run dev` 启动后端
|
||||
|
||||
### 前端
|
||||
|
||||
后端使用 [Next.js](https://nextjs.org) 建立,使用 Typescript
|
||||
|
||||
#### 搭建
|
||||
|
||||
1. 首先启动后端
|
||||
2. 打开 `frontend` 文件夹
|
||||
3. 通过 `npm install` 安装依赖
|
||||
4. 通过 `npm run dev` 启动前端
|
||||
|
||||
开发项目配置完成
|
||||
|
||||
### 测试
|
||||
|
||||
目前阶段我们只有后端的系统测试,在 `backend` 文件夹运行 `npm run test:system` 来执行系统测试
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
*Leer esto en otro idioma: [Inglés](README.md), [Español](README.es.md)*
|
||||
_Leer esto en otro idioma: [Inglés](/README.md), [Español](/docs/README.es.md), [Chino Simplificado](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
126
docs/README.zh-cn.md
Normal file
126
docs/README.zh-cn.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_选择合适的语言阅读: [西班牙语](/docs/README.es.md), [英语](/README.md), [简体中文](/docs/README.zh-cn.md)_
|
||||
|
||||
---
|
||||
|
||||
Pingvin Share 是一个可自建的文件分享平台,是 WeTransfer 的一个替代品
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 通过可自定义后缀的链接分享文件
|
||||
- 可自定义任意大小的文件上传限制 (受制于托管所在的硬盘大小)
|
||||
- 对共享链接设置有效期限
|
||||
- 对共享链接设置访问次数和访问密码
|
||||
- 通过邮件自动发送共享链接
|
||||
- 整合 ClamAV 进行反病毒检查
|
||||
|
||||
## 🐧 了解一下 Pingvin Share
|
||||
|
||||
- [示例网站](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [DB Tech 推荐视频](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
## ⌨️ 自建指南
|
||||
|
||||
> 注意:Pingvin Share 仍处于开发阶段并且可能存在 bugs
|
||||
|
||||
### Docker 部署 (推荐)
|
||||
|
||||
1. 下载 `docker-compose.yml`
|
||||
2. 运行命令 `docker-compose up -d`
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### Stand-alone 部署
|
||||
|
||||
必须的依赖:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 16
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) 用于后台运行 Pingvin Share
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stonith404/pingvin-share
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
现在网站运行在 `http://localhost:3000`,尝试一下你本地的 Pingvin Share 🐧!
|
||||
|
||||
### 整合组件
|
||||
|
||||
#### ClamAV (仅限 Docker 部署)
|
||||
|
||||
扫描上传文件中是否存在可疑文件,如果存在 ClamAV 会自动移除
|
||||
|
||||
1. 在 docker-compose 配置中添加 ClamAV 容器 (见 `docker-compose.yml` 注释部分) 并启动容器
|
||||
2. Docker 会在启动 Pingvin Share 前启动 ClamAV,也许会花费 1-2 分钟
|
||||
3. Pingvin Share 日志中应该有 "ClamAV is active"
|
||||
|
||||
请注意 ClamAV 会消耗很多 [系统资源(特别是内存)](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements)
|
||||
|
||||
### 更多资源
|
||||
|
||||
- [群晖 NAS 配置](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### 升级
|
||||
|
||||
因为 Pingvin Share 仍处在开发阶段,在升级前请务必阅读 release notes 避免不可逆的改变
|
||||
|
||||
#### Docker 升级
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Stand-alone 升级
|
||||
|
||||
1. 停止正在运行的 app
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. 重复 [installation guide](#stand-alone-installation) 中的步骤,除了 `git clone` 这一步
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# 获取最新的版本
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# 启动后端 backend
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
# 启动前端 frontend
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### 自定义品牌
|
||||
|
||||
你可以在管理员配置页面改变网站的名字和 logo
|
||||
|
||||
## 🖤 提交贡献
|
||||
|
||||
非常欢迎向 Pingvin Share 提交贡献! 请阅读 [contribution guide](/CONTRIBUTING.md) 来提交你的贡献
|
||||
@@ -19,4 +19,7 @@ module.exports = withPWA({
|
||||
output: "standalone", env: {
|
||||
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",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"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);
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
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 { 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) => ({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
@@ -34,6 +35,8 @@ const MyShares = () => {
|
||||
|
||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||
|
||||
const appUrl = config.get("general.appUrl");
|
||||
|
||||
const getReverseShares = () => {
|
||||
shareService
|
||||
.getMyReverseShares()
|
||||
@@ -119,9 +122,11 @@ const MyShares = () => {
|
||||
<Accordion.Panel>
|
||||
{reverseShare.shares.map((share) => (
|
||||
<Group key={share.id} mb={4}>
|
||||
<Anchor href={`${appUrl}/share/${share.id}`}>
|
||||
<Text maw={120} truncate>
|
||||
{share.id}
|
||||
</Text>
|
||||
</Anchor>
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
@@ -129,9 +134,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."
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
MediaQuery,
|
||||
Space,
|
||||
Stack,
|
||||
Table,
|
||||
@@ -15,7 +16,7 @@ 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 { TbLink, TbTrash, TbInfoCircle } from "react-icons/tb";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import CenterLoader from "../../components/core/CenterLoader";
|
||||
import Meta from "../../components/Meta";
|
||||
@@ -23,6 +24,7 @@ import useConfig from "../../hooks/config.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyShare } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -12,16 +12,7 @@ import {
|
||||
import api from "./api.service";
|
||||
|
||||
const create = async (share: CreateShare) => {
|
||||
const { id, expiration, recipients, security, description } = share;
|
||||
return (
|
||||
await api.post("shares", {
|
||||
id,
|
||||
expiration,
|
||||
recipients,
|
||||
security,
|
||||
description,
|
||||
})
|
||||
).data;
|
||||
return (await api.post("shares", share)).data;
|
||||
};
|
||||
|
||||
const completeShare = async (id: string) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export type ShareMetaData = {
|
||||
|
||||
export type MyShare = Share & {
|
||||
views: number;
|
||||
cratedAt: Date;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type MyReverseShare = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"scripts": {
|
||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||
|
||||
Reference in New Issue
Block a user