Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c7457697 | ||
|
|
1abc0f7ef3 | ||
|
|
2c3760e064 | ||
|
|
32eaee4236 | ||
|
|
99492c2ecc | ||
|
|
34db3ae2a9 | ||
|
|
32ad43ae27 | ||
|
|
0efd2d8bf9 | ||
|
|
43299522ee |
11
.env.example
11
.env.example
@@ -1,11 +1,18 @@
|
|||||||
# Read what every environment variable does: https://github.com/stonith404/pingvin-share#environment-variables
|
# Read what every environment variable does: https://github.com/stonith404/pingvin-share#environment-variables
|
||||||
|
|
||||||
# GENERAL
|
# General
|
||||||
APP_URL=http://localhost:3000
|
APP_URL=http://localhost:3000
|
||||||
SHOW_HOME_PAGE=true
|
SHOW_HOME_PAGE=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
||||||
MAX_FILE_SIZE=1000000000
|
MAX_FILE_SIZE=1000000000
|
||||||
|
|
||||||
# SECURITY
|
# Security
|
||||||
JWT_SECRET=long-random-string
|
JWT_SECRET=long-random-string
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_RECIPIENTS_ENABLED=false
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_EMAIL=pingvin-share@example.com
|
||||||
|
SMTP_PASSWORD=example
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
|||||||
|
## [0.2.0](https://github.com/stonith404/pingvin-share/compare/v0.1.1...v0.2.0) (2022-11-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add email recepients functionality ([32ad43a](https://github.com/stonith404/pingvin-share/commit/32ad43ae27a29b946bfba0040cac7eb158c84553))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add public userDTO to prevent confusion ([0efd2d8](https://github.com/stonith404/pingvin-share/commit/0efd2d8bf96506cf7d7dc2dc3164a8d59009cec7))
|
||||||
|
* email sending when not signed in ([32eaee4](https://github.com/stonith404/pingvin-share/commit/32eaee42363250defa92913c738a2702ba3e2693))
|
||||||
|
* hide and disallow email recipients if disabled ([34db3ae](https://github.com/stonith404/pingvin-share/commit/34db3ae2a997498edaa70404807d0e770dad6edb))
|
||||||
|
|
||||||
### [0.1.1](https://github.com/stonith404/pingvin-share/compare/v0.1.0...v0.1.1) (2022-10-31)
|
### [0.1.1](https://github.com/stonith404/pingvin-share/compare/v0.1.0...v0.1.1) (2022-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -14,6 +14,7 @@ Demo: https://pingvin-share.dev.eliasschneider.com
|
|||||||
- 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
|
||||||
- Light & dark mode
|
- Light & dark mode
|
||||||
|
|
||||||
## ⌨️ Setup
|
## ⌨️ Setup
|
||||||
@@ -28,14 +29,16 @@ The website is now listening available on `http://localhost:3000`, have fun with
|
|||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Description | Possible values |
|
| Variable | Description | Possible values |
|
||||||
| ------------------------------ | ------------------------------------------------------------------------------------------- | --------------- |
|
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------- |
|
||||||
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
|
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
|
||||||
| `SHOW_HOME_PAGE` | Whether the Pingvin Share home page should be shown. | true/false |
|
| `SHOW_HOME_PAGE` | Whether the Pingvin Share home page should be shown. | true/false |
|
||||||
| `ALLOW_REGISTRATION` | Whether a new user can create a new account. | true/false |
|
| `ALLOW_REGISTRATION` | Whether a new user can create a new account. | true/false |
|
||||||
| `ALLOW_UNAUTHENTICATED_SHARES` | Whether a user can create a share without being signed in. | true/false |
|
| `ALLOW_UNAUTHENTICATED_SHARES` | Whether a user can create a share without being signed in. | true/false |
|
||||||
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
|
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
|
||||||
| `JWT_SECRET` | Long random string to sign the JWT's. | Random string |
|
| `JWT_SECRET` | Long random string to sign the JWT's. | Random string |
|
||||||
|
| `EMAIL_RECIPIENTS_ENABLED` | Whether email reciepients are enabled. Only set this to true if you entered the host, port, email and password of your SMTP server. | true/false |
|
||||||
|
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_EMAIL`, `SMTP_PASSWORD` | Credentials for your SMTP server. | - |
|
||||||
|
|
||||||
### Upgrade to a new version
|
### Upgrade to a new version
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
# CONFIGURATION
|
# General
|
||||||
APP_URL=http://localhost:3000
|
APP_URL=http://localhost:3000
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
MAX_FILE_SIZE=5000000000
|
MAX_FILE_SIZE=5000000000
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
||||||
|
|
||||||
# SECURITY
|
# Security
|
||||||
JWT_SECRET=random-string
|
JWT_SECRET=random-string
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_RECIPIENTS_ENABLED=false
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_EMAIL=pingvin-share@example.com
|
||||||
|
SMTP_PASSWORD=example
|
||||||
33
backend/package-lock.json
generated
33
backend/package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.7.23",
|
"@types/node": "^18.7.23",
|
||||||
|
"@types/nodemailer": "^6.4.6",
|
||||||
"@types/passport-jwt": "^3.0.7",
|
"@types/passport-jwt": "^3.0.7",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||||
@@ -1214,6 +1216,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
||||||
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz",
|
||||||
|
"integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
@@ -5028,6 +5039,14 @@
|
|||||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
|
||||||
|
"integrity": "sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
@@ -8303,6 +8322,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
|
||||||
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
"integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg=="
|
||||||
},
|
},
|
||||||
|
"@types/nodemailer": {
|
||||||
|
"version": "6.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz",
|
||||||
|
"integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
@@ -11224,6 +11252,11 @@
|
|||||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"version": "6.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
|
||||||
|
"integrity": "sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ=="
|
||||||
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.7.23",
|
"@types/node": "^18.7.23",
|
||||||
|
"@types/nodemailer": "^6.4.6",
|
||||||
"@types/passport-jwt": "^3.0.7",
|
"@types/passport-jwt": "^3.0.7",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ShareRecipient" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"shareId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "ShareRecipient_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "Share" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Share" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"uploadLocked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isZipReady" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"views" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"expiration" DATETIME NOT NULL,
|
||||||
|
"creatorId" TEXT,
|
||||||
|
CONSTRAINT "Share_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Share" ("createdAt", "creatorId", "expiration", "id", "isZipReady", "uploadLocked", "views") SELECT "createdAt", "creatorId", "expiration", "id", "isZipReady", "uploadLocked", "views" FROM "Share";
|
||||||
|
DROP TABLE "Share";
|
||||||
|
ALTER TABLE "new_Share" RENAME TO "Share";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -40,10 +40,19 @@ model Share {
|
|||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
expiration DateTime
|
expiration DateTime
|
||||||
|
|
||||||
creatorId String?
|
creatorId String?
|
||||||
creator User? @relation(fields: [creatorId], references: [id])
|
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||||
security ShareSecurity?
|
security ShareSecurity?
|
||||||
files File[]
|
recipients ShareRecipient[]
|
||||||
|
files File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ShareRecipient {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String
|
||||||
|
|
||||||
|
shareId String
|
||||||
|
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import { PrismaService } from "./prisma/prisma.service";
|
|||||||
import { ShareController } from "./share/share.controller";
|
import { ShareController } from "./share/share.controller";
|
||||||
import { ShareModule } from "./share/share.module";
|
import { ShareModule } from "./share/share.module";
|
||||||
import { UserController } from "./user/user.controller";
|
import { UserController } from "./user/user.controller";
|
||||||
|
import { EmailModule } from "./email/email.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
ShareModule,
|
ShareModule,
|
||||||
FileModule,
|
FileModule,
|
||||||
|
EmailModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
ThrottlerModule.forRoot({
|
ThrottlerModule.forRoot({
|
||||||
|
|||||||
8
backend/src/email/email.module.ts
Normal file
8
backend/src/email/email.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { EmailService } from "./email.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [EmailService],
|
||||||
|
exports: [EmailService],
|
||||||
|
})
|
||||||
|
export class EmailModule {}
|
||||||
38
backend/src/email/email.service.ts
Normal file
38
backend/src/email/email.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import * as nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailService {
|
||||||
|
constructor(private config: ConfigService) {}
|
||||||
|
|
||||||
|
// create reusable transporter object using the default SMTP transport
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: this.config.get("SMTP_HOST"),
|
||||||
|
port: parseInt(this.config.get("SMTP_PORT")),
|
||||||
|
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
|
||||||
|
auth: {
|
||||||
|
user: this.config.get("SMTP_EMAIL"),
|
||||||
|
pass: this.config.get("SMTP_PASSWORD"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async sendMail(recipientEmail: string, shareId: string, creator: User) {
|
||||||
|
if (this.config.get("EMAIL_RECIPIENTS_ENABLED") == "false")
|
||||||
|
throw new InternalServerErrorException("Email service disabled");
|
||||||
|
|
||||||
|
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
|
||||||
|
const creatorIdentifier = creator ?
|
||||||
|
creator.firstName && creator.lastName
|
||||||
|
? `${creator.firstName} ${creator.lastName}`
|
||||||
|
: creator.email : "A Pingvin Share user";
|
||||||
|
|
||||||
|
await this.transporter.sendMail({
|
||||||
|
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
|
||||||
|
to: recipientEmail,
|
||||||
|
subject: "Files shared with you",
|
||||||
|
text: `Hey!\n${creatorIdentifier} shared some files with you. View or dowload the files with this link: ${shareUrl}.\n Shared securely with Pingvin Share 🐧`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsString, Length, Matches, ValidateNested } from "class-validator";
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
Matches,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
import { ShareSecurityDTO } from "./shareSecurity.dto";
|
import { ShareSecurityDTO } from "./shareSecurity.dto";
|
||||||
|
|
||||||
export class CreateShareDTO {
|
export class CreateShareDTO {
|
||||||
@@ -13,6 +19,9 @@ export class CreateShareDTO {
|
|||||||
@IsString()
|
@IsString()
|
||||||
expiration: string;
|
expiration: string;
|
||||||
|
|
||||||
|
@IsEmail({}, { each: true })
|
||||||
|
recipients: string[];
|
||||||
|
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => ShareSecurityDTO)
|
@Type(() => ShareSecurityDTO)
|
||||||
security: ShareSecurityDTO;
|
security: ShareSecurityDTO;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export class MyShareDTO extends ShareDTO {
|
|||||||
@Expose()
|
@Expose()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
recipients: string[];
|
||||||
|
|
||||||
from(partial: Partial<MyShareDTO>) {
|
from(partial: Partial<MyShareDTO>) {
|
||||||
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Expose, plainToClass, Type } from "class-transformer";
|
import { Expose, plainToClass, Type } from "class-transformer";
|
||||||
import { AuthSignInDTO } from "src/auth/dto/authSignIn.dto";
|
|
||||||
import { FileDTO } from "src/file/dto/file.dto";
|
import { FileDTO } from "src/file/dto/file.dto";
|
||||||
|
import { PublicUserDTO } from "src/user/dto/publicUser.dto";
|
||||||
|
|
||||||
export class ShareDTO {
|
export class ShareDTO {
|
||||||
@Expose()
|
@Expose()
|
||||||
@@ -14,8 +14,8 @@ export class ShareDTO {
|
|||||||
files: FileDTO[];
|
files: FileDTO[];
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => AuthSignInDTO)
|
@Type(() => PublicUserDTO)
|
||||||
creator: AuthSignInDTO;
|
creator: PublicUserDTO;
|
||||||
|
|
||||||
from(partial: Partial<ShareDTO>) {
|
from(partial: Partial<ShareDTO>) {
|
||||||
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { forwardRef, Module } from "@nestjs/common";
|
import { forwardRef, Module } from "@nestjs/common";
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
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({}), forwardRef(() => FileModule)],
|
imports: [JwtModule.register({}), EmailModule, forwardRef(() => FileModule)],
|
||||||
controllers: [ShareController],
|
controllers: [ShareController],
|
||||||
providers: [ShareService],
|
providers: [ShareService],
|
||||||
exports: [ShareService],
|
exports: [ShareService],
|
||||||
|
|||||||
@@ -11,6 +11,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 { 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 { CreateShareDTO } from "./dto/createShare.dto";
|
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||||
@@ -20,6 +21,7 @@ export class ShareService {
|
|||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
|
private emailService: EmailService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private jwtService: JwtService
|
private jwtService: JwtService
|
||||||
) {}
|
) {}
|
||||||
@@ -36,7 +38,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have to add an exception for "never" (since moment won't like that)
|
// We have to add an exception for "never" (since moment won't like that)
|
||||||
let expirationDate;
|
let expirationDate: Date;
|
||||||
if (share.expiration !== "never") {
|
if (share.expiration !== "never") {
|
||||||
expirationDate = moment()
|
expirationDate = moment()
|
||||||
.add(
|
.add(
|
||||||
@@ -60,6 +62,11 @@ export class ShareService {
|
|||||||
expiration: expirationDate,
|
expiration: expirationDate,
|
||||||
creator: { connect: user ? { id: user.id } : undefined },
|
creator: { connect: user ? { id: user.id } : undefined },
|
||||||
security: { create: share.security },
|
security: { create: share.security },
|
||||||
|
recipients: {
|
||||||
|
create: share.recipients
|
||||||
|
? share.recipients.map((email) => ({ email }))
|
||||||
|
: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -84,21 +91,33 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async complete(id: string) {
|
async complete(id: string) {
|
||||||
|
const share = await this.prisma.share.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { files: true, recipients: true, creator: true },
|
||||||
|
});
|
||||||
|
|
||||||
if (await this.isShareCompleted(id))
|
if (await this.isShareCompleted(id))
|
||||||
throw new BadRequestException("Share already completed");
|
throw new BadRequestException("Share already completed");
|
||||||
|
|
||||||
const moreThanOneFileInShare =
|
if (share.files.length == 0)
|
||||||
(await this.prisma.file.findMany({ where: { shareId: id } })).length != 0;
|
|
||||||
|
|
||||||
if (!moreThanOneFileInShare)
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"You need at least on file in your share to complete it."
|
"You need at least on file in your share to complete it."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Asynchronously create a zip of all files
|
||||||
this.createZip(id).then(() =>
|
this.createZip(id).then(() =>
|
||||||
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
|
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Send email for each recepient
|
||||||
|
for (const recepient of share.recipients) {
|
||||||
|
await this.emailService.sendMail(
|
||||||
|
recepient.email,
|
||||||
|
share.id,
|
||||||
|
share.creator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.prisma.share.update({
|
return await this.prisma.share.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { uploadLocked: true },
|
data: { uploadLocked: true },
|
||||||
@@ -106,7 +125,7 @@ export class ShareService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSharesByUser(userId: string) {
|
async getSharesByUser(userId: string) {
|
||||||
return await this.prisma.share.findMany({
|
const shares = await this.prisma.share.findMany({
|
||||||
where: {
|
where: {
|
||||||
creator: { id: userId },
|
creator: { id: userId },
|
||||||
uploadLocked: true,
|
uploadLocked: true,
|
||||||
@@ -119,7 +138,17 @@ export class ShareService {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
expiration: "desc",
|
expiration: "desc",
|
||||||
},
|
},
|
||||||
|
include: { recipients: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sharesWithEmailRecipients = shares.map((share) => {
|
||||||
|
return {
|
||||||
|
...share,
|
||||||
|
recipients: share.recipients.map((recipients) => recipients.email),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sharesWithEmailRecipients;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(id: string) {
|
||||||
|
|||||||
4
backend/src/user/dto/publicUser.dto.ts
Normal file
4
backend/src/user/dto/publicUser.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PickType } from "@nestjs/swagger";
|
||||||
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
|
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {}
|
||||||
@@ -12,5 +12,10 @@ services:
|
|||||||
- ALLOW_UNAUTHENTICATED_SHARES=${ALLOW_UNAUTHENTICATED_SHARES}
|
- ALLOW_UNAUTHENTICATED_SHARES=${ALLOW_UNAUTHENTICATED_SHARES}
|
||||||
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
|
- MAX_FILE_SIZE=${MAX_FILE_SIZE}
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- EMAIL_RECIPIENTS_ENABLED=${EMAIL_RECIPIENTS_ENABLED}
|
||||||
|
- SMTP_HOST=${SMTP_HOST}
|
||||||
|
- SMTP_PORT=${SMTP_PORT}
|
||||||
|
- SMTP_EMAIL=${SMTP_EMAIL}
|
||||||
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- "${PWD}/data:/opt/app/backend/data"
|
- "${PWD}/data:/opt/app/backend/data"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
SHOW_HOME_PAGE=true
|
SHOW_HOME_PAGE=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
MAX_FILE_SIZE=1000000000
|
MAX_FILE_SIZE=1000000000
|
||||||
ALLOW_UNAUTHENTICATED_SHARES=false
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
||||||
|
EMAIL_RECIPIENTS_ENABLED=false
|
||||||
@@ -5,7 +5,8 @@ const nextConfig = {
|
|||||||
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
|
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
|
||||||
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
|
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
|
||||||
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
|
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
|
||||||
ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES
|
ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES,
|
||||||
|
EMAIL_RECIPIENTS_ENABLED: process.env.EMAIL_RECIPIENTS_ENABLED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1496
frontend/package-lock.json
generated
1496
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,12 +23,12 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jose": "^4.8.1",
|
"jose": "^4.8.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"next": "^12.3.1",
|
"next": "^13.0.0",
|
||||||
"next-cookies": "^2.0.3",
|
"next-cookies": "^2.0.3",
|
||||||
"next-http-proxy-middleware": "^1.2.4",
|
"next-http-proxy-middleware": "^1.2.4",
|
||||||
"next-pwa": "^5.6.0",
|
"next-pwa": "^5.6.0",
|
||||||
"react": "18.0.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "18.0.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
"eslint": "8.13.0",
|
"eslint": "8.13.0",
|
||||||
"eslint-config-next": "12.1.5",
|
"eslint-config-next": "^13.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.11",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { NextLink } from "@mantine/next";
|
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
|
import Link from "next/link";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
@@ -61,7 +61,7 @@ const AuthForm = ({ mode }: { mode: "signUp" | "signIn" }) => {
|
|||||||
? "You have an account already?"
|
? "You have an account already?"
|
||||||
: "You don't have an account yet?"}{" "}
|
: "You don't have an account yet?"}{" "}
|
||||||
<Anchor
|
<Anchor
|
||||||
component={NextLink}
|
component={Link}
|
||||||
href={mode == "signUp" ? "signIn" : "signUp"}
|
href={mode == "signUp" ? "signIn" : "signUp"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
||||||
import { NextLink } from "@mantine/next";
|
import Link from "next/link";
|
||||||
import { TbDoorExit, TbLink } from "react-icons/tb";
|
import { TbDoorExit, TbLink } from "react-icons/tb";
|
||||||
import authService from "../../services/auth.service";
|
import authService from "../../services/auth.service";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const ActionAvatar = () => {
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={NextLink}
|
component={Link}
|
||||||
href="/account/shares"
|
href="/account/shares"
|
||||||
icon={<TbLink size={14} />}
|
icon={<TbLink size={14} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
Transition,
|
Transition,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { NextLink } from "@mantine/next";
|
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
|
import Link from "next/link";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import useUser from "../../hooks/user.hook";
|
import useUser from "../../hooks/user.hook";
|
||||||
import Logo from "../Logo";
|
import Logo from "../Logo";
|
||||||
@@ -22,7 +22,7 @@ const { publicRuntimeConfig } = getConfig();
|
|||||||
|
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
type Link = {
|
type NavLink = {
|
||||||
link?: string;
|
link?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
component?: ReactNode;
|
component?: ReactNode;
|
||||||
@@ -122,7 +122,7 @@ const NavBar = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const [unauthenticatedLinks, setUnauthenticatedLinks] = useState<Link[]>([
|
const [unauthenticatedLinks, setUnauthenticatedLinks] = useState<NavLink[]>([
|
||||||
{
|
{
|
||||||
link: "/auth/signIn",
|
link: "/auth/signIn",
|
||||||
label: "Sign in",
|
label: "Sign in",
|
||||||
@@ -161,7 +161,7 @@ const NavBar = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<NextLink
|
<Link
|
||||||
key={link.label}
|
key={link.label}
|
||||||
href={link.link ?? ""}
|
href={link.link ?? ""}
|
||||||
onClick={() => toggleOpened.toggle()}
|
onClick={() => toggleOpened.toggle()}
|
||||||
@@ -170,7 +170,7 @@ const NavBar = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</NextLink>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
@@ -178,12 +178,12 @@ const NavBar = () => {
|
|||||||
return (
|
return (
|
||||||
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
|
||||||
<Container className={classes.header}>
|
<Container className={classes.header}>
|
||||||
<NextLink href="/">
|
<Link href="/" passHref>
|
||||||
<Group>
|
<Group>
|
||||||
<Logo height={35} width={35} />
|
<Logo height={35} width={35} />
|
||||||
<Text weight={600}>Pingvin Share</Text>
|
<Text weight={600}>Pingvin Share</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</NextLink>
|
</Link>
|
||||||
<Group spacing={5} className={classes.links}>
|
<Group spacing={5} className={classes.links}>
|
||||||
<Group>{items} </Group>
|
<Group>{items} </Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ 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 getConfig from "next/config";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbCopy } from "react-icons/tb";
|
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 { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const showCompletedUploadModal = (modals: ModalsContextProps, share: Share) => {
|
const showCompletedUploadModal = (modals: ModalsContextProps, share: Share) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
closeOnClickOutside: false,
|
closeOnClickOutside: false,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
MultiSelect,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Select,
|
Select,
|
||||||
@@ -33,6 +34,7 @@ const showCreateUploadModal = (
|
|||||||
uploadCallback: (
|
uploadCallback: (
|
||||||
id: string,
|
id: string,
|
||||||
expiration: string,
|
expiration: string,
|
||||||
|
recipients: string[],
|
||||||
security: ShareSecurity
|
security: ShareSecurity
|
||||||
) => void
|
) => void
|
||||||
) => {
|
) => {
|
||||||
@@ -54,6 +56,7 @@ const CreateUploadModalBody = ({
|
|||||||
uploadCallback: (
|
uploadCallback: (
|
||||||
id: string,
|
id: string,
|
||||||
expiration: string,
|
expiration: string,
|
||||||
|
recipients: string[],
|
||||||
security: ShareSecurity
|
security: ShareSecurity
|
||||||
) => void;
|
) => void;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
@@ -79,7 +82,7 @@ const CreateUploadModalBody = ({
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
link: "",
|
link: "",
|
||||||
|
recipients: [] as string[],
|
||||||
password: undefined,
|
password: undefined,
|
||||||
maxViews: undefined,
|
maxViews: undefined,
|
||||||
expiration_num: 1,
|
expiration_num: 1,
|
||||||
@@ -110,7 +113,7 @@ const CreateUploadModalBody = ({
|
|||||||
const expiration = form.values.never_expires
|
const expiration = form.values.never_expires
|
||||||
? "never"
|
? "never"
|
||||||
: form.values.expiration_num + form.values.expiration_unit;
|
: form.values.expiration_num + form.values.expiration_unit;
|
||||||
uploadCallback(values.link, expiration, {
|
uploadCallback(values.link, expiration, values.recipients, {
|
||||||
password: values.password,
|
password: values.password,
|
||||||
maxViews: values.maxViews,
|
maxViews: values.maxViews,
|
||||||
});
|
});
|
||||||
@@ -211,7 +214,6 @@ const CreateUploadModalBody = ({
|
|||||||
label="Never Expires"
|
label="Never Expires"
|
||||||
{...form.getInputProps("never_expires")}
|
{...form.getInputProps("never_expires")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Preview expiration date text */}
|
{/* Preview expiration date text */}
|
||||||
<Text
|
<Text
|
||||||
italic
|
italic
|
||||||
@@ -222,8 +224,37 @@ const CreateUploadModalBody = ({
|
|||||||
>
|
>
|
||||||
{ExpirationPreview({ form })}
|
{ExpirationPreview({ form })}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Accordion>
|
<Accordion>
|
||||||
|
{publicRuntimeConfig.EMAIL_RECIPIENTS_ENABLED == "true" && (
|
||||||
|
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
|
||||||
|
<Accordion.Control>Email recipients</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<MultiSelect
|
||||||
|
data={form.values.recipients}
|
||||||
|
placeholder="Enter email recipients"
|
||||||
|
searchable
|
||||||
|
{...form.getInputProps("recipients")}
|
||||||
|
creatable
|
||||||
|
getCreateLabel={(query) => `+ ${query}`}
|
||||||
|
onCreate={(query) => {
|
||||||
|
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
||||||
|
form.setFieldError(
|
||||||
|
"recipients",
|
||||||
|
"Invalid email address"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
form.setFieldError("recipients", null);
|
||||||
|
form.setFieldValue("recipients", [
|
||||||
|
...form.values.recipients,
|
||||||
|
query,
|
||||||
|
]);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
)}
|
||||||
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Security options</Accordion.Control>
|
<Accordion.Control>Security options</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
import { NextLink } from "@mantine/next";
|
import Link from "next/link";
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -53,7 +53,7 @@ const ErrorNotFound = () => {
|
|||||||
className={classes.description}
|
className={classes.description}
|
||||||
></Text>
|
></Text>
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Button component={NextLink} href="/" variant="light">
|
<Button component={Link} href="/" variant="light">
|
||||||
Bring me back
|
Bring me back
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { NextLink } from "@mantine/next";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import getConfig from "next/config";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbLink, TbTrash } from "react-icons/tb";
|
import { TbLink, TbTrash } from "react-icons/tb";
|
||||||
@@ -24,8 +23,6 @@ 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";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
|
||||||
|
|
||||||
const MyShares = () => {
|
const MyShares = () => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
@@ -54,7 +51,7 @@ const MyShares = () => {
|
|||||||
<Title order={3}>It's empty here 👀</Title>
|
<Title order={3}>It's empty here 👀</Title>
|
||||||
<Text>You don't have any shares.</Text>
|
<Text>You don't have any shares.</Text>
|
||||||
<Space h={5} />
|
<Space h={5} />
|
||||||
<Button component={NextLink} href="/upload" variant="light">
|
<Button component={Link} href="/upload" variant="light">
|
||||||
Create one
|
Create one
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { NextLink } from "@mantine/next";
|
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbCheck } from "react-icons/tb";
|
import { TbCheck } from "react-icons/tb";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
@@ -126,7 +126,7 @@ export default function Home() {
|
|||||||
|
|
||||||
<Group mt={30}>
|
<Group mt={30}>
|
||||||
<Button
|
<Button
|
||||||
component={NextLink}
|
component={Link}
|
||||||
href="/auth/signUp"
|
href="/auth/signUp"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -135,7 +135,7 @@ export default function Home() {
|
|||||||
Get started
|
Get started
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
component={NextLink}
|
component={Link}
|
||||||
href="https://github.com/stonith404/pingvin-share"
|
href="https://github.com/stonith404/pingvin-share"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const Upload = () => {
|
|||||||
const uploadFiles = async (
|
const uploadFiles = async (
|
||||||
id: string,
|
id: string,
|
||||||
expiration: string,
|
expiration: string,
|
||||||
|
recipients: string[],
|
||||||
security: ShareSecurity
|
security: ShareSecurity
|
||||||
) => {
|
) => {
|
||||||
setisUploading(true);
|
setisUploading(true);
|
||||||
@@ -39,7 +40,7 @@ const Upload = () => {
|
|||||||
return file;
|
return file;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
share = await shareService.create(id, expiration, security);
|
share = await shareService.create(id, expiration, recipients, security);
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const progressCallBack = (progress: number) => {
|
const progressCallBack = (progress: number) => {
|
||||||
setFiles((files) => {
|
setFiles((files) => {
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import api from "./api.service";
|
|||||||
const create = async (
|
const create = async (
|
||||||
id: string,
|
id: string,
|
||||||
expiration: string,
|
expiration: string,
|
||||||
|
recipients: string[],
|
||||||
security?: ShareSecurity
|
security?: ShareSecurity
|
||||||
) => {
|
) => {
|
||||||
return (await api.post("shares", { id, expiration, security })).data;
|
return (await api.post("shares", { id, expiration, recipients, security }))
|
||||||
|
.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeShare = async (id: string) => {
|
const completeShare = async (id: string) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Global } from "@mantine/core";
|
|||||||
const GlobalStyle = () => {
|
const GlobalStyle = () => {
|
||||||
return (
|
return (
|
||||||
<Global
|
<Global
|
||||||
styles={(theme) => ({
|
styles={() => ({
|
||||||
a: {
|
a: {
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pingvin-share",
|
"name": "pingvin-share",
|
||||||
"version": "0.1.1",
|
"version": "0.2.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