Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09213a295 | ||
|
|
fc116d65c0 | ||
|
|
76088cc76a | ||
|
|
16b697053a | ||
|
|
349bf475cc | ||
|
|
fccc4cbc02 | ||
|
|
f1b44f87fa | ||
|
|
02e41e2437 | ||
|
|
74e8956106 |
1
.github/workflows/close_inactive_issues.yml
vendored
1
.github/workflows/close_inactive_issues.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
days-before-issue-stale: 30
|
days-before-issue-stale: 30
|
||||||
days-before-issue-close: 14
|
days-before-issue-close: 14
|
||||||
|
exempt-issue-labels: "feature"
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||||
|
|||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
|||||||
|
## [0.7.0](https://github.com/stonith404/pingvin-share/compare/v0.6.1...v0.7.0) (2023-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ClamAV to scan for malicious files ([76088cc](https://github.com/stonith404/pingvin-share/commit/76088cc76aedae709f06deaee2244efcf6a22bed))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* invalid github release link on admin page ([349bf47](https://github.com/stonith404/pingvin-share/commit/349bf475cc7fc1141dbd2a9bd2f63153c4d5b41b))
|
||||||
|
|
||||||
|
### [0.6.1](https://github.com/stonith404/pingvin-share/compare/v0.6.0...v0.6.1) (2023-01-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* delete all sessions if password was changed ([02e41e2](https://github.com/stonith404/pingvin-share/commit/02e41e243768de34de1bdc8833e83f60db530e55))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* shareUrl uses wrong origin ([f1b44f8](https://github.com/stonith404/pingvin-share/commit/f1b44f87fa64d3b21ca92c9068cb352d0ad51bc0))
|
||||||
|
* update password doesn't work ([74e8956](https://github.com/stonith404/pingvin-share/commit/74e895610642552c98c0015d0f8347735aaed457))
|
||||||
|
|
||||||
## [0.6.0](https://github.com/stonith404/pingvin-share/compare/v0.5.1...v0.6.0) (2023-01-09)
|
## [0.6.0](https://github.com/stonith404/pingvin-share/compare/v0.5.1...v0.6.0) (2023-01-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN npm run build && npm prune --production
|
|||||||
|
|
||||||
# Stage 5: Final image
|
# Stage 5: Final image
|
||||||
FROM node:18-slim AS runner
|
FROM node:18-slim AS runner
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=docker
|
||||||
RUN apt-get update && apt-get install -y openssl
|
RUN apt-get update && apt-get install -y openssl
|
||||||
|
|
||||||
WORKDIR /opt/app/frontend
|
WORKDIR /opt/app/frontend
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -4,13 +4,12 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- Spin up your instance within 2 minutes
|
|
||||||
- Create a share with files that you can access with a link
|
- Create a share with files that you can access with a link
|
||||||
- No file size limit, only your disk will be your limit
|
- No file size limit, only your disk will be your limit
|
||||||
- Set a share expiration
|
- Set a share expiration
|
||||||
- Optionally secure your share with a visitor limit and a password
|
- Optionally secure your share with a visitor limit and a password
|
||||||
- Email recepients
|
- Email recepients
|
||||||
- Light & dark mode
|
- ClamAV integration
|
||||||
|
|
||||||
## 🐧 Get to know Pingvin Share
|
## 🐧 Get to know Pingvin Share
|
||||||
|
|
||||||
@@ -30,6 +29,18 @@ Pingvin Share is self-hosted file sharing platform and an alternative for WeTran
|
|||||||
|
|
||||||
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
The website is now listening available on `http://localhost:3000`, have fun with Pingvin Share 🐧!
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
|
||||||
|
#### ClamAV
|
||||||
|
|
||||||
|
With ClamAV the shares get scanned for malicious files and get removed if any found.
|
||||||
|
|
||||||
|
1. Add the ClamAV container to the Docker Compose stack (see `docker-compose.yml`) and start the container.
|
||||||
|
2. As soon as the ClamAV container is ready (when ClamAV logs "socket found, clamd started"), restart the Pingvin Share container with `docker compose restart pingvin-share`
|
||||||
|
3. The Pingvin Share logs should now log "ClamAV is active"
|
||||||
|
|
||||||
|
Please note that ClamAV needs a lot of [ressources](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||||
|
|
||||||
### Additional resources
|
### Additional resources
|
||||||
|
|
||||||
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||||
|
|||||||
1361
backend/package-lock.json
generated
1361
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-backend",
|
"name": "pingvin-share-backend",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"dev": "nest start --watch",
|
"dev": "nest start --watch",
|
||||||
@@ -16,60 +16,62 @@
|
|||||||
"@nestjs/common": "^9.2.1",
|
"@nestjs/common": "^9.2.1",
|
||||||
"@nestjs/config": "^2.2.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"@nestjs/core": "^9.2.1",
|
"@nestjs/core": "^9.2.1",
|
||||||
"@nestjs/jwt": "^9.0.0",
|
"@nestjs/jwt": "^10.0.1",
|
||||||
"@nestjs/mapped-types": "^1.2.0",
|
"@nestjs/mapped-types": "^1.2.0",
|
||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.0",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/schedule": "^2.1.0",
|
"@nestjs/schedule": "^2.1.0",
|
||||||
"@nestjs/throttler": "^3.1.0",
|
"@nestjs/throttler": "^3.1.0",
|
||||||
"@prisma/client": "^4.7.1",
|
"@prisma/client": "^4.8.1",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"argon2": "^0.30.2",
|
"argon2": "^0.30.3",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.20.1",
|
||||||
|
"clamscan": "^2.1.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.9.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^4.0.4",
|
||||||
"rxjs": "^7.6.0",
|
"rxjs": "^7.8.0",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.1.5",
|
"@nestjs/cli": "^9.1.8",
|
||||||
"@nestjs/schematics": "^9.0.3",
|
"@nestjs/schematics": "^9.0.4",
|
||||||
"@nestjs/testing": "^9.2.1",
|
"@nestjs/testing": "^9.2.1",
|
||||||
"@types/archiver": "^5.3.1",
|
"@types/archiver": "^5.3.1",
|
||||||
|
"@types/clamscan": "^2.0.4",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.15",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/node": "^18.11.10",
|
"@types/node": "^18.11.18",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.7",
|
||||||
"@types/passport-jwt": "^3.0.7",
|
"@types/passport-jwt": "^3.0.8",
|
||||||
"@types/qrcode-svg": "^1.1.1",
|
"@types/qrcode-svg": "^1.1.1",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"newman": "^5.3.2",
|
"newman": "^5.3.2",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.2",
|
||||||
"prisma": "^4.7.1",
|
"prisma": "^4.8.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
"tsconfig-paths": "4.1.1",
|
"tsconfig-paths": "4.1.2",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.4",
|
||||||
"wait-on": "^6.0.1"
|
"wait-on": "^7.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Share" ADD COLUMN "removedReason" TEXT;
|
||||||
@@ -52,11 +52,12 @@ model Share {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
uploadLocked Boolean @default(false)
|
uploadLocked Boolean @default(false)
|
||||||
isZipReady Boolean @default(false)
|
isZipReady Boolean @default(false)
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
expiration DateTime
|
expiration DateTime
|
||||||
description String?
|
description String?
|
||||||
|
removedReason String?
|
||||||
|
|
||||||
creatorId String?
|
creatorId String?
|
||||||
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { JobsModule } from "./jobs/jobs.module";
|
|||||||
import { PrismaModule } from "./prisma/prisma.module";
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
import { ShareModule } from "./share/share.module";
|
import { ShareModule } from "./share/share.module";
|
||||||
import { UserModule } from "./user/user.module";
|
import { UserModule } from "./user/user.module";
|
||||||
|
import { ClamscanModule } from "./clamscan/clamscan.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,6 +29,7 @@ import { UserModule } from "./user/user.module";
|
|||||||
limit: 100,
|
limit: 100,
|
||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
|
ClamscanModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
|||||||
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
import { AuthSignInDTO } from "./dto/authSignIn.dto";
|
||||||
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
|
||||||
import { EnableTotpDTO } from "./dto/enableTotp.dto";
|
import { EnableTotpDTO } from "./dto/enableTotp.dto";
|
||||||
|
import { TokenDTO } from "./dto/token.dto";
|
||||||
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
|
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
|
||||||
import { VerifyTotpDTO } from "./dto/verifyTotp.dto";
|
import { VerifyTotpDTO } from "./dto/verifyTotp.dto";
|
||||||
import { JwtGuard } from "./guard/jwt.guard";
|
import { JwtGuard } from "./guard/jwt.guard";
|
||||||
@@ -45,8 +46,8 @@ export class AuthController {
|
|||||||
|
|
||||||
response = this.addTokensToResponse(
|
response = this.addTokensToResponse(
|
||||||
response,
|
response,
|
||||||
result.accessToken,
|
result.refreshToken,
|
||||||
result.refreshToken
|
result.accessToken
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -64,8 +65,8 @@ export class AuthController {
|
|||||||
if (result.accessToken && result.refreshToken) {
|
if (result.accessToken && result.refreshToken) {
|
||||||
response = this.addTokensToResponse(
|
response = this.addTokensToResponse(
|
||||||
response,
|
response,
|
||||||
result.accessToken,
|
result.refreshToken,
|
||||||
result.refreshToken
|
result.accessToken
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,17 +84,28 @@ export class AuthController {
|
|||||||
|
|
||||||
response = this.addTokensToResponse(
|
response = this.addTokensToResponse(
|
||||||
response,
|
response,
|
||||||
result.accessToken,
|
result.refreshToken,
|
||||||
result.refreshToken
|
result.accessToken
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return new TokenDTO().from(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("password")
|
@Patch("password")
|
||||||
@UseGuards(JwtGuard)
|
@UseGuards(JwtGuard)
|
||||||
async updatePassword(@GetUser() user: User, @Body() dto: UpdatePasswordDTO) {
|
async updatePassword(
|
||||||
await this.authService.updatePassword(user, dto.oldPassword, dto.password);
|
@GetUser() user: User,
|
||||||
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
@Body() dto: UpdatePasswordDTO
|
||||||
|
) {
|
||||||
|
const result = await this.authService.updatePassword(
|
||||||
|
user,
|
||||||
|
dto.oldPassword,
|
||||||
|
dto.password
|
||||||
|
);
|
||||||
|
|
||||||
|
response = this.addTokensToResponse(response, result.refreshToken);
|
||||||
|
return new TokenDTO().from(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("token")
|
@Post("token")
|
||||||
@@ -108,7 +120,7 @@ export class AuthController {
|
|||||||
request.cookies.refresh_token
|
request.cookies.refresh_token
|
||||||
);
|
);
|
||||||
response.cookie("access_token", accessToken);
|
response.cookie("access_token", accessToken);
|
||||||
return { accessToken };
|
return new TokenDTO().from({ accessToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("signOut")
|
@Post("signOut")
|
||||||
@@ -146,15 +158,16 @@ export class AuthController {
|
|||||||
|
|
||||||
private addTokensToResponse(
|
private addTokensToResponse(
|
||||||
response: Response,
|
response: Response,
|
||||||
accessToken: string,
|
refreshToken?: string,
|
||||||
refreshToken: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
response.cookie("access_token", accessToken);
|
if (accessToken) response.cookie("access_token", accessToken);
|
||||||
response.cookie("refresh_token", refreshToken, {
|
if (refreshToken)
|
||||||
path: "/api/auth/token",
|
response.cookie("refresh_token", refreshToken, {
|
||||||
httpOnly: true,
|
path: "/api/auth/token",
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
|
httpOnly: true,
|
||||||
});
|
maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,10 +87,16 @@ export class AuthService {
|
|||||||
|
|
||||||
const hash = await argon.hash(newPassword);
|
const hash = await argon.hash(newPassword);
|
||||||
|
|
||||||
this.prisma.user.update({
|
await this.prisma.refreshToken.deleteMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { password: hash },
|
data: { password: hash },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.createRefreshToken(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAccessToken(user: User, refreshTokenId: string) {
|
async createAccessToken(user: User, refreshTokenId: string) {
|
||||||
@@ -112,7 +118,12 @@ export class AuthService {
|
|||||||
refreshTokenId: string;
|
refreshTokenId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.prisma.refreshToken.delete({ where: { id: refreshTokenId } });
|
await this.prisma.refreshToken
|
||||||
|
.delete({ where: { id: refreshTokenId } })
|
||||||
|
.catch((e) => {
|
||||||
|
// Ignore error if refresh token doesn't exist
|
||||||
|
if (e.code != "P2025") throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshAccessToken(refreshToken: string) {
|
async refreshAccessToken(refreshToken: string) {
|
||||||
|
|||||||
15
backend/src/auth/dto/token.dto.ts
Normal file
15
backend/src/auth/dto/token.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
export class TokenDTO {
|
||||||
|
@Expose()
|
||||||
|
accessToken: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
refreshToken: string;
|
||||||
|
|
||||||
|
from(partial: Partial<TokenDTO>) {
|
||||||
|
return plainToClass(TokenDTO, partial, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/src/clamscan/clamscan.module.ts
Normal file
10
backend/src/clamscan/clamscan.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { forwardRef, Module } from "@nestjs/common";
|
||||||
|
import { FileModule } from "src/file/file.module";
|
||||||
|
import { ClamScanService } from "./clamscan.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [forwardRef(() => FileModule)],
|
||||||
|
providers: [ClamScanService],
|
||||||
|
exports: [ClamScanService],
|
||||||
|
})
|
||||||
|
export class ClamscanModule {}
|
||||||
86
backend/src/clamscan/clamscan.service.ts
Normal file
86
backend/src/clamscan/clamscan.service.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable } 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";
|
||||||
|
|
||||||
|
const clamscanConfig = {
|
||||||
|
clamdscan: {
|
||||||
|
host: process.env.NODE_ENV == "docker" ? "clamav" : "127.0.0.1",
|
||||||
|
port: 3310,
|
||||||
|
localFallback: false,
|
||||||
|
},
|
||||||
|
preference: "clamdscan",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClamScanService {
|
||||||
|
constructor(
|
||||||
|
private fileService: FileService,
|
||||||
|
private prisma: PrismaService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private ClamScan: Promise<NodeClam | null> = new NodeClam()
|
||||||
|
.init(clamscanConfig)
|
||||||
|
.then((res) => {
|
||||||
|
console.log("ClamAV is active");
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log("ClamAV is not active");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async check(shareId: string) {
|
||||||
|
const clamScan = await this.ClamScan;
|
||||||
|
|
||||||
|
if (!clamScan) return [];
|
||||||
|
|
||||||
|
const infectedFiles = [];
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(`./data/uploads/shares/${shareId}`)
|
||||||
|
.filter((file) => file != "archive.zip");
|
||||||
|
|
||||||
|
for (const fileId of files) {
|
||||||
|
const { isInfected } = await clamScan
|
||||||
|
.isInfected(`./data/uploads/shares/${shareId}/${fileId}`)
|
||||||
|
.catch(() => {
|
||||||
|
console.log("ClamAV is not active");
|
||||||
|
return { isInfected: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = (
|
||||||
|
await this.prisma.file.findUnique({ where: { id: fileId } })
|
||||||
|
).name;
|
||||||
|
|
||||||
|
if (isInfected) {
|
||||||
|
infectedFiles.push({ id: fileId, name: fileName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infectedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAndRemove(shareId: string) {
|
||||||
|
const infectedFiles = await this.check(shareId);
|
||||||
|
|
||||||
|
if (infectedFiles.length > 0) {
|
||||||
|
await this.fileService.deleteAllFiles(shareId);
|
||||||
|
await this.prisma.file.deleteMany({ where: { shareId } });
|
||||||
|
|
||||||
|
const fileNames = infectedFiles.map((file) => file.name).join(", ");
|
||||||
|
|
||||||
|
await this.prisma.share.update({
|
||||||
|
where: { id: shareId },
|
||||||
|
data: {
|
||||||
|
removedReason: `Your share got removed because the file(s) ${fileNames} are malicious.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ async function bootstrap() {
|
|||||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
||||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||||
|
|
||||||
app.use(bodyParser.raw({type:'application/octet-stream', limit:'20mb'}));
|
app.use(bodyParser.raw({ type: "application/octet-stream", limit: "20mb" }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.set("trust proxy", true);
|
app.set("trust proxy", true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { forwardRef, Module } from "@nestjs/common";
|
import { forwardRef, Module } from "@nestjs/common";
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
import { ClamscanModule } from "src/clamscan/clamscan.module";
|
||||||
import { EmailModule } from "src/email/email.module";
|
import { EmailModule } from "src/email/email.module";
|
||||||
import { FileModule } from "src/file/file.module";
|
import { FileModule } from "src/file/file.module";
|
||||||
import { ShareController } from "./share.controller";
|
import { ShareController } from "./share.controller";
|
||||||
import { ShareService } from "./share.service";
|
import { ShareService } from "./share.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [JwtModule.register({}), EmailModule, forwardRef(() => FileModule)],
|
imports: [
|
||||||
|
JwtModule.register({}),
|
||||||
|
EmailModule,
|
||||||
|
ClamscanModule,
|
||||||
|
forwardRef(() => FileModule),
|
||||||
|
],
|
||||||
controllers: [ShareController],
|
controllers: [ShareController],
|
||||||
providers: [ShareService],
|
providers: [ShareService],
|
||||||
exports: [ShareService],
|
exports: [ShareService],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as archiver from "archiver";
|
|||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
import { ClamScanService } from "src/clamscan/clamscan.service";
|
||||||
import { ConfigService } from "src/config/config.service";
|
import { ConfigService } from "src/config/config.service";
|
||||||
import { EmailService } from "src/email/email.service";
|
import { EmailService } from "src/email/email.service";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "src/file/file.service";
|
||||||
@@ -23,7 +24,8 @@ export class ShareService {
|
|||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
private emailService: EmailService,
|
private emailService: EmailService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private jwtService: JwtService
|
private jwtService: JwtService,
|
||||||
|
private clasmScanService: ClamScanService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(share: CreateShareDTO, user?: User) {
|
async create(share: CreateShareDTO, user?: User) {
|
||||||
@@ -123,6 +125,9 @@ export class ShareService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if any file is malicious with ClamAV
|
||||||
|
this.clasmScanService.checkAndRemove(share.id);
|
||||||
|
|
||||||
return await this.prisma.share.update({
|
return await this.prisma.share.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { uploadLocked: true },
|
data: { uploadLocked: true },
|
||||||
@@ -157,7 +162,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(id: string) {
|
||||||
const share: any = await this.prisma.share.findUnique({
|
const share = await this.prisma.share.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
files: true,
|
files: true,
|
||||||
@@ -165,10 +170,13 @@ export class ShareService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (share.removedReason)
|
||||||
|
throw new NotFoundException(share.removedReason, "share_removed");
|
||||||
|
|
||||||
if (!share || !share.uploadLocked)
|
if (!share || !share.uploadLocked)
|
||||||
throw new NotFoundException("Share not found");
|
throw new NotFoundException("Share not found");
|
||||||
|
|
||||||
return share;
|
return share as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetaData(id: string) {
|
async getMetaData(id: string) {
|
||||||
|
|||||||
7
docker-compose-dev.yml
Normal file
7
docker-compose-dev.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
clamav:
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 3310:3310
|
||||||
|
image: clamav/clamav
|
||||||
@@ -6,4 +6,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
volumes:
|
volumes:
|
||||||
- "${PWD}/data:/opt/app/backend/data"
|
- "./data:/opt/app/backend/data"
|
||||||
|
# Optional: Add ClamAV (see README.md)
|
||||||
|
# ClamAV is currently only available for AMD64 see https://github.com/Cisco-Talos/clamav/issues/482
|
||||||
|
# clamav:
|
||||||
|
# restart: unless-stopped
|
||||||
|
# image: clamav/clamav
|
||||||
723
frontend/package-lock.json
generated
723
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share-frontend",
|
"name": "pingvin-share-frontend",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -11,19 +11,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
"@mantine/core": "^5.9.2",
|
"@mantine/core": "^5.10.0",
|
||||||
"@mantine/dropzone": "^5.9.2",
|
"@mantine/dropzone": "^5.10.0",
|
||||||
"@mantine/form": "^5.9.2",
|
"@mantine/form": "^5.10.0",
|
||||||
"@mantine/hooks": "^5.9.2",
|
"@mantine/hooks": "^5.10.0",
|
||||||
"@mantine/modals": "^5.9.2",
|
"@mantine/modals": "^5.10.0",
|
||||||
"@mantine/next": "^5.9.2",
|
"@mantine/next": "^5.10.0",
|
||||||
"@mantine/notifications": "^5.9.2",
|
"@mantine/notifications": "^5.10.0",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.2",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jose": "^4.11.1",
|
"jose": "^4.11.2",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "^13.0.6",
|
"next": "^13.1.2",
|
||||||
"next-cookies": "^2.0.3",
|
"next-cookies": "^2.0.3",
|
||||||
"next-http-proxy-middleware": "^1.2.5",
|
"next-http-proxy-middleware": "^1.2.5",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
@@ -34,15 +34,15 @@
|
|||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.11.10",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "18.0.10",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.2",
|
||||||
"eslint": "8.29.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-config-next": "^13.0.6",
|
"eslint-config-next": "^13.1.2",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.2",
|
||||||
"tar": "^6.1.12",
|
"tar": "^6.1.13",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Stack, TextInput } from "@mantine/core";
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
|
||||||
const showShareLinkModal = (modals: ModalsContextProps, shareId: string) => {
|
const showShareLinkModal = (
|
||||||
const link = `${window.location.origin}/share/${shareId}`;
|
modals: ModalsContextProps,
|
||||||
|
shareId: string,
|
||||||
|
appUrl: string
|
||||||
|
) => {
|
||||||
|
const link = `${appUrl}/share/${shareId}`;
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: "Share link",
|
title: "Share link",
|
||||||
children: (
|
children: (
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ const Dropzone = ({
|
|||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
openRef={openRef as ForwardedRef<() => void>}
|
openRef={openRef as ForwardedRef<() => void>}
|
||||||
onDrop={(newFiles: FileUpload[]) => {
|
onDrop={(newFiles: FileUpload[]) => {
|
||||||
const fileSizeSum = [...newFiles, ...files].reduce((n, { size }) => n + size, 0);
|
const fileSizeSum = [...newFiles, ...files].reduce(
|
||||||
|
(n, { size }) => n + size,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
if (fileSizeSum > config.get("MAX_SHARE_SIZE")) {
|
if (fileSizeSum > config.get("MAX_SHARE_SIZE")) {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ 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 toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const showCompletedUploadModal = (modals: ModalsContextProps, share: Share) => {
|
const showCompletedUploadModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
share: Share,
|
||||||
|
appUrl: string
|
||||||
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
withCloseButton: false,
|
withCloseButton: false,
|
||||||
@@ -25,15 +29,16 @@ const showCompletedUploadModal = (modals: ModalsContextProps, share: Share) => {
|
|||||||
<Title order={4}>Share ready</Title>
|
<Title order={4}>Share ready</Title>
|
||||||
</Stack>
|
</Stack>
|
||||||
),
|
),
|
||||||
children: <Body share={share} />,
|
children: <Body share={share} appUrl={appUrl} />,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Body = ({ share }: { share: Share }) => {
|
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const link = `${window.location.origin}/share/${share.id}`;
|
|
||||||
|
const link = `${appUrl}/share/${share.id}`;
|
||||||
return (
|
return (
|
||||||
<Stack align="stretch">
|
<Stack align="stretch">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const showCreateUploadModal = (
|
|||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
options: {
|
options: {
|
||||||
isUserSignedIn: boolean;
|
isUserSignedIn: boolean;
|
||||||
|
appUrl: string;
|
||||||
allowUnauthenticatedShares: boolean;
|
allowUnauthenticatedShares: boolean;
|
||||||
enableEmailRecepients: boolean;
|
enableEmailRecepients: boolean;
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,7 @@ const CreateUploadModalBody = ({
|
|||||||
uploadCallback: (createShare: CreateShare) => void;
|
uploadCallback: (createShare: CreateShare) => void;
|
||||||
options: {
|
options: {
|
||||||
isUserSignedIn: boolean;
|
isUserSignedIn: boolean;
|
||||||
|
appUrl: string;
|
||||||
allowUnauthenticatedShares: boolean;
|
allowUnauthenticatedShares: boolean;
|
||||||
enableEmailRecepients: boolean;
|
enableEmailRecepients: boolean;
|
||||||
};
|
};
|
||||||
@@ -156,7 +158,7 @@ const CreateUploadModalBody = ({
|
|||||||
color: theme.colors.gray[6],
|
color: theme.colors.gray[6],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{window.location.origin}/share/
|
{options.appUrl}/share/
|
||||||
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||||
</Text>
|
</Text>
|
||||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { TbLink, TbTrash } from "react-icons/tb";
|
import { TbLink, TbTrash } from "react-icons/tb";
|
||||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.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";
|
||||||
@@ -28,6 +29,8 @@ const MyShares = () => {
|
|||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const [shares, setShares] = useState<MyShare[]>();
|
const [shares, setShares] = useState<MyShare[]>();
|
||||||
@@ -86,13 +89,17 @@ const MyShares = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
clipboard.copy(
|
clipboard.copy(
|
||||||
`${window.location.origin}/share/${share.id}`
|
`${config.get("APP_URL")}/share/${share.id}`
|
||||||
);
|
);
|
||||||
toast.success(
|
toast.success(
|
||||||
"Your link was copied to the keyboard."
|
"Your link was copied to the keyboard."
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showShareLinkModal(modals, share.id);
|
showShareLinkModal(
|
||||||
|
modals,
|
||||||
|
share.id,
|
||||||
|
config.get("APP_URL")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const Admin = () => {
|
|||||||
title: "Update",
|
title: "Update",
|
||||||
icon: TbRefresh,
|
icon: TbRefresh,
|
||||||
route:
|
route:
|
||||||
"https://github.com/stonith404/pingvin-share/releases/tag/v0.5.0",
|
"https://github.com/stonith404/pingvin-share/releases/latest",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,21 +47,19 @@ const Share = ({ shareId }: { shareId: string }) => {
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const { error } = e.response.data;
|
const { error } = e.response.data;
|
||||||
if (e.response.status == 404) {
|
if (e.response.status == 404) {
|
||||||
showErrorModal(
|
if (error == "share_removed") {
|
||||||
modals,
|
showErrorModal(modals, "Share removed", e.response.data.message);
|
||||||
"Not found",
|
} else {
|
||||||
"This share can't be found. Please check your link."
|
showErrorModal(
|
||||||
);
|
modals,
|
||||||
|
"Not found",
|
||||||
|
"This share can't be found. Please check your link."
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (error == "share_password_required") {
|
} else if (error == "share_password_required") {
|
||||||
showEnterPasswordModal(modals, getShareToken);
|
showEnterPasswordModal(modals, getShareToken);
|
||||||
} else if (error == "share_token_required") {
|
} else if (error == "share_token_required") {
|
||||||
getShareToken();
|
getShareToken();
|
||||||
} else if (error == "forbidden") {
|
|
||||||
showErrorModal(
|
|
||||||
modals,
|
|
||||||
"Forbidden",
|
|
||||||
"You're not allowed to see this share. Are you logged in with the correct account?"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showErrorModal(modals, "Error", "An unknown error occurred.");
|
showErrorModal(modals, "Error", "An unknown error occurred.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const Upload = () => {
|
|||||||
.completeShare(createdShare.id)
|
.completeShare(createdShare.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setisUploading(false);
|
setisUploading(false);
|
||||||
showCompletedUploadModal(modals, createdShare);
|
showCompletedUploadModal(modals, createdShare, config.get("APP_URL"));
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
@@ -164,6 +164,7 @@ const Upload = () => {
|
|||||||
modals,
|
modals,
|
||||||
{
|
{
|
||||||
isUserSignedIn: user ? true : false,
|
isUserSignedIn: user ? true : false,
|
||||||
|
appUrl: config.get("APP_URL"),
|
||||||
allowUnauthenticatedShares: config.get(
|
allowUnauthenticatedShares: config.get(
|
||||||
"ALLOW_UNAUTHENTICATED_SHARES"
|
"ALLOW_UNAUTHENTICATED_SHARES"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type FileUpload = File & { uploadingProgress: number };
|
export type FileUpload = File & { uploadingProgress: number };
|
||||||
|
|
||||||
export type FileUploadResponse = {id: string, name: string}
|
export type FileUploadResponse = { id: string; name: string };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.6.0",
|
"version": "0.7.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