From ce19d22c68fc738d8ce315e88a364d6d97dd31de Mon Sep 17 00:00:00 2001
From: Elias Schneider
Date: Fri, 6 May 2022 10:25:10 +0200
Subject: [PATCH] Add feature to send share with email
---
.env.example | 3 +-
.setup/data/collections.ts | 15 +-
.setup/data/functions.ts | 6 +
.setup/index.ts | 2 +-
README.md | 8 +
docker-compose.yml | 3 +-
functions/createShare/package.json | 3 +-
functions/createShare/src/index.js | 15 +-
functions/createShare/src/util.js | 25 ++-
package-lock.json | 31 ++--
package.json | 6 +-
.../share/CreateUploadModalBody.tsx | 166 ++++++++++++++++++
.../upload/showCompletedUploadModal.tsx | 12 +-
.../upload/showCreateUploadModal.tsx | 139 +--------------
src/pages/api/share/[shareId]/index.ts | 39 ++--
src/pages/api/user/exists/[email].ts | 12 ++
src/pages/share/[shareId].tsx | 15 +-
src/pages/upload.tsx | 26 ++-
src/services/auth.service.ts | 8 +
src/services/share.service.ts | 11 +-
src/types/Appwrite.type.ts | 8 +-
src/utils/toast.util.tsx | 1 +
22 files changed, 365 insertions(+), 189 deletions(-)
create mode 100644 src/components/share/CreateUploadModalBody.tsx
create mode 100644 src/pages/api/user/exists/[email].ts
create mode 100644 src/services/auth.service.ts
diff --git a/.env.example b/.env.example
index 6387efd..353af8f 100644
--- a/.env.example
+++ b/.env.example
@@ -2,4 +2,5 @@ APPWRITE_FUNCTION_API_KEY=""
PUBLIC_APPWRITE_HOST="http://localhost/v1"
PUBLIC_MAX_FILE_SIZE="300000000" # Note: Must be the same as in the _APP_STORAGE_LIMIT in the Appwrite env file
PUBLIC_DISABLE_REGISTRATION="true" # Note: In the Appwrite console you have to change your user limit to 0 if false and else to 1
-PUBLIC_DISABLE_HOME_PAGE="false"
\ No newline at end of file
+PUBLIC_DISABLE_HOME_PAGE="false"
+PUBLIC_MAIL_SHARE_ENABLED="false"
\ No newline at end of file
diff --git a/.setup/data/collections.ts b/.setup/data/collections.ts
index 57cc220..e4d295f 100644
--- a/.setup/data/collections.ts
+++ b/.setup/data/collections.ts
@@ -10,16 +10,22 @@ export default [
{
key: "securityID",
type: "string",
- status: "available",
required: false,
array: false,
size: 255,
default: null,
},
+ {
+ key: "users",
+ type: "string",
+ required: false,
+ array: true,
+ size: 255,
+ default: null,
+ },
{
key: "createdAt",
type: "integer",
- status: "available",
required: true,
array: false,
min: 0,
@@ -29,7 +35,6 @@ export default [
{
key: "expiresAt",
type: "integer",
- status: "available",
required: true,
array: false,
min: 0,
@@ -39,7 +44,6 @@ export default [
{
key: "visitorCount",
type: "integer",
- status: "available",
required: false,
array: false,
min: 0,
@@ -49,7 +53,6 @@ export default [
{
key: "enabled",
type: "boolean",
- status: "available",
required: false,
array: false,
default: false,
@@ -75,7 +78,6 @@ export default [
{
key: "password",
type: "string",
- status: "available",
required: false,
array: false,
size: 128,
@@ -84,7 +86,6 @@ export default [
{
key: "maxVisitors",
type: "integer",
- status: "available",
required: false,
array: false,
min: 0,
diff --git a/.setup/data/functions.ts b/.setup/data/functions.ts
index d682d83..ac4a90b 100644
--- a/.setup/data/functions.ts
+++ b/.setup/data/functions.ts
@@ -12,6 +12,12 @@ export default () => {
vars: {
APPWRITE_FUNCTION_ENDPOINT: host,
APPWRITE_FUNCTION_API_KEY: process.env["APPWRITE_FUNCTION_API_KEY"],
+ SMTP_HOST: "",
+ SMTP_PORT: "",
+ SMTP_USER: "",
+ SMTP_PASSWORD: "",
+ SMTP_FROM: "",
+ FRONTEND_URL: "",
},
events: [],
schedule: "",
diff --git a/.setup/index.ts b/.setup/index.ts
index 4c70f78..3ae4d2e 100644
--- a/.setup/index.ts
+++ b/.setup/index.ts
@@ -41,7 +41,7 @@ import rl from "readline-sync";
console.info("Creating function deployments...");
await setupService.createFunctionDeployments();
- console.info("Adding frontend url...");
+ console.info("Adding frontend host...");
await setupService.addPlatform(
rl.question("Frontend host of Pingvin Share (localhost): ", {
defaultInput: "localhost",
diff --git a/README.md b/README.md
index fce3428..48dd19f 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,14 @@ You're almost done, now you have to change your environment variables that they
3. Change `PUBLIC_APPWRITE_HOST` in the `.env` file to the host where your Appwrite instance runs
4. Change `PUBLIC_MAX_FILE_SIZE` in the `.env` file to the max file size limit you want
+## Additional configurations
+
+### SMTP
+
+1. Enable `PUBLIC_MAIL_SHARE_ENABLE` in the `.env` file.
+2. Visit your Appwrite console, click on functions and select the `Create Share` function.
+3. At the settings tab change the empty variables to your SMTP setup.
+
## Known issues / Limitations
Pingvin Share is currently in beta and there are issues and limitations that should be fixed in the future.
diff --git a/docker-compose.yml b/docker-compose.yml
index 561e12f..4e2db4a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,4 +10,5 @@ services:
- PUBLIC_APPWRITE_HOST=${PUBLIC_APPWRITE_HOST}
- PUBLIC_MAX_FILE_SIZE=${PUBLIC_MAX_FILE_SIZE}
- PUBLIC_DISABLE_REGISTRATION=${PUBLIC_DISABLE_REGISTRATION}
- - PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE}
\ No newline at end of file
+ - PUBLIC_DISABLE_HOME_PAGE=${PUBLIC_DISABLE_HOME_PAGE}
+ - PUBLIC_MAIL_SHARE_ENABLED=${PUBLIC_MAIL_SHARE_ENABLED}
\ No newline at end of file
diff --git a/functions/createShare/package.json b/functions/createShare/package.json
index 6295c61..709ab05 100644
--- a/functions/createShare/package.json
+++ b/functions/createShare/package.json
@@ -7,6 +7,7 @@
"author": "",
"license": "ISC",
"dependencies": {
- "node-appwrite": "^5.0.0"
+ "node-appwrite": "^5.0.0",
+ "nodemailer": "^6.7.5"
}
}
diff --git a/functions/createShare/src/index.js b/functions/createShare/src/index.js
index abd2bcb..01ef8f5 100644
--- a/functions/createShare/src/index.js
+++ b/functions/createShare/src/index.js
@@ -4,8 +4,8 @@ const util = require("./util")
module.exports = async function (req, res) {
const client = new sdk.Client();
- // You can remove services you don't use
let database = new sdk.Database(client);
+ let users = new sdk.Users(client);
let storage = new sdk.Storage(client);
client
@@ -34,6 +34,18 @@ module.exports = async function (req, res) {
).$id;
}
+ let userIds;
+ if (payload.emails) {
+ const creatorEmail = (await users.get(userId)).email
+ userIds = []
+ userIds.push(userId)
+ for (const email of payload.emails) {
+ userIds.push((await users.list(`email='${email}'`)).users[0].$id)
+ util.sendMail(email, creatorEmail, payload.id)
+ }
+
+ }
+
// Create the storage bucket
await storage.createBucket(
payload.id,
@@ -48,6 +60,7 @@ module.exports = async function (req, res) {
// Create document in Shares collection
await database.createDocument("shares", payload.id, {
securityID: securityDocumentId,
+ users: userIds,
createdAt: Date.now(),
expiresAt: expiration,
});
diff --git a/functions/createShare/src/util.js b/functions/createShare/src/util.js
index ff34abe..5a72abf 100644
--- a/functions/createShare/src/util.js
+++ b/functions/createShare/src/util.js
@@ -1,9 +1,32 @@
const { scryptSync } = require("crypto");
+const mail = require("nodemailer")
+
+
+const transporter = mail.createTransport({
+ host: process.env["SMTP_HOST"],
+ port: process.env["SMTP_PORT"],
+ secure: false,
+ auth: {
+ user: process.env["SMTP_USER"],
+ pass: process.env["SMTP_PASSWORD"],
+ },
+});
const hashPassword = (password, salt) => {
return scryptSync(password, salt, 64).toString("hex");
}
+const sendMail = (receiver, creatorEmail, shareId) => {
+ let message = {
+ from: process.env["SMTP_FROM"],
+ to: receiver,
+ subject: "New share from Pingvin Share",
+ text: `Hey, ${creatorEmail} shared files with you. To access the files, visit ${process.env.FRONTEND_URL}/share/${shareId}`
+
+ }
+ transporter.sendMail(message)
+}
+
module.exports = {
- hashPassword,
+ hashPassword, sendMail
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f65e183..558c9dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,9 +17,9 @@
"@mantine/notifications": "^4.2.0",
"appwrite": "^7.0.0",
"axios": "^0.26.1",
- "cookie": "^0.5.0",
"cookies-next": "^2.0.4",
"file-saver": "^2.0.5",
+ "jose": "^4.8.1",
"js-file-download": "^0.4.12",
"jszip": "^3.9.1",
"next": "12.1.5",
@@ -40,7 +40,6 @@
"@types/tar": "^6.1.1",
"@types/uuid": "^8.3.4",
"axios": "^0.26.1",
- "cookie": "^0.5.0",
"eslint": "8.13.0",
"eslint-config-next": "12.1.5",
"node-appwrite": "^5.1.0",
@@ -3345,15 +3344,6 @@
"safe-buffer": "~5.1.1"
}
},
- "node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
- "dev": true,
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/cookies-next": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.0.4.tgz",
@@ -5279,6 +5269,14 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/jose": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz",
+ "integrity": "sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-file-download": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz",
@@ -10210,12 +10208,6 @@
"safe-buffer": "~5.1.1"
}
},
- "cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
- "dev": true
- },
"cookies-next": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.0.4.tgz",
@@ -11658,6 +11650,11 @@
}
}
},
+ "jose": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz",
+ "integrity": "sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw=="
+ },
"js-file-download": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz",
diff --git a/package.json b/package.json
index 11af5fb..6d6e85e 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "deploy": "docker buildx build -t stonith404/pingvin-share --platform linux/amd64,linux/arm64 --push ."
+ "deploy:latest": "docker buildx build -t stonith404/pingvin-share:latest --platform linux/amd64,linux/arm64 --push .",
+ "deploy:development": "docker buildx build -t stonith404/pingvin-share:development --platform linux/amd64,linux/arm64 --push ."
},
"dependencies": {
"@mantine/core": "^4.2.0",
@@ -19,9 +20,9 @@
"@mantine/notifications": "^4.2.0",
"appwrite": "^7.0.0",
"axios": "^0.26.1",
- "cookie": "^0.5.0",
"cookies-next": "^2.0.4",
"file-saver": "^2.0.5",
+ "jose": "^4.8.1",
"js-file-download": "^0.4.12",
"jszip": "^3.9.1",
"next": "12.1.5",
@@ -42,7 +43,6 @@
"@types/tar": "^6.1.1",
"@types/uuid": "^8.3.4",
"axios": "^0.26.1",
- "cookie": "^0.5.0",
"eslint": "8.13.0",
"eslint-config-next": "12.1.5",
"node-appwrite": "^5.1.0",
diff --git a/src/components/share/CreateUploadModalBody.tsx b/src/components/share/CreateUploadModalBody.tsx
new file mode 100644
index 0000000..fdc3caf
--- /dev/null
+++ b/src/components/share/CreateUploadModalBody.tsx
@@ -0,0 +1,166 @@
+import {
+ Accordion,
+ Button,
+ Col,
+ Grid,
+ Group,
+ MultiSelect,
+ NumberInput,
+ PasswordInput,
+ Select,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useForm, yupResolver } from "@mantine/form";
+import { useModals } from "@mantine/modals";
+import * as yup from "yup";
+import shareService from "../../services/share.service";
+import toast from "../../utils/toast.util";
+
+const CreateUploadModalBody = ({
+ mode,
+ uploadCallback,
+}: {
+ mode: "standard" | "email";
+ uploadCallback: (
+ id: string,
+ expiration: number,
+ security: { password?: string; maxVisitors?: number },
+ emails?: string[]
+ ) => void;
+}) => {
+ const modals = useModals();
+ const validationSchema = yup.object().shape({
+ link: yup.string().required().min(2).max(50),
+ emails: mode == "email" ? yup.array().of(yup.string().email()).min(1) : yup.array(),
+ password: yup.string().min(3).max(100),
+ maxVisitors: yup.number().min(1),
+ });
+ const form = useForm({
+ initialValues: {
+ link: "",
+ emails: [] as string[],
+ password: undefined,
+ maxVisitors: undefined,
+ expiration: "1440",
+ },
+ schema: yupResolver(validationSchema),
+ });
+
+ return (
+
+ );
+};
+
+export default CreateUploadModalBody;
diff --git a/src/components/upload/showCompletedUploadModal.tsx b/src/components/upload/showCompletedUploadModal.tsx
index a7eb4f9..c869514 100644
--- a/src/components/upload/showCompletedUploadModal.tsx
+++ b/src/components/upload/showCompletedUploadModal.tsx
@@ -16,13 +16,21 @@ import toast from "../../utils/toast.util";
const showCompletedUploadModal = (
modals: ModalsContextProps,
link: string,
- expiresAt: string
+ expiresAt: string,
+ mode: "email" | "standard"
) => {
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
- title: Share ready,
+ title: (
+
+ Share ready
+ {mode == "email" && (
+ Emails were sent to the to invited users.
+ )}
+
+ ),
children: ,
});
};
diff --git a/src/components/upload/showCreateUploadModal.tsx b/src/components/upload/showCreateUploadModal.tsx
index b6e45c0..852845e 100644
--- a/src/components/upload/showCreateUploadModal.tsx
+++ b/src/components/upload/showCreateUploadModal.tsx
@@ -1,145 +1,22 @@
-import {
- Accordion,
- Button,
- Col,
- Grid,
- Group,
- NumberInput,
- PasswordInput,
- Select,
- Text,
- TextInput,
- Title,
-} from "@mantine/core";
-import { useForm, yupResolver } from "@mantine/form";
-import { useModals } from "@mantine/modals";
+import { Title } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
-import * as yup from "yup";
-import shareService from "../../services/share.service";
+import CreateUploadModalBody from "../share/CreateUploadModalBody";
const showCreateUploadModal = (
+ mode: "standard" | "email",
modals: ModalsContextProps,
uploadCallback: (
id: string,
expiration: number,
- security: { password?: string; maxVisitors?: number }
+ security: { password?: string; maxVisitors?: number}, emails? : string[]
) => void
) => {
return modals.openModal({
title:
Share,
- children: