diff --git a/.env.example b/.env.example index 6387efd2..353af8f0 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 57cc220d..e4d295f8 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 d682d83c..ac4a90b5 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 4c70f786..3ae4d2e1 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 fce3428d..48dd19f3 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 561e12f6..4e2db4af 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 6295c618..709ab051 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 abd2bcba..01ef8f57 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 ff34abe3..5a72abfd 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 f65e183c..558c9ddf 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 11af5fb4..6d6e85e1 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 00000000..fdc3caf0 --- /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 a7eb4f9f..c8695149 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: