├── .env ├── .gitignore ├── README.md ├── package.json ├── public └── client-side.js └── server.js /.env: -------------------------------------------------------------------------------- 1 | CLOUDNAME=yourcloudnamehere 2 | CLOUDAPIKEY=yourapikeyhere 3 | CLOUDINARYSECRET=yourapisecrethere -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | To visit the app in your browser you can use a username of `admin` and a password of `admin`. 2 | 3 | # Important Note 4 | 5 | Please, please, please do **not** accidentally leak your environment variables in your .env file on your Github / GitLab / elsewhere. 6 | 7 | 99.99% of the time you should use .gitignore to exclude your .env file from your repository. 8 | 9 | I only included it in this example because it's super common for people following lessons to include a typo in the variable name for environment variables, and then never be able to figure out why there example is not working. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-cloudinary", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon server.js", 8 | "start": "node server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cloudinary": "^1.30.0", 16 | "dotenv": "^16.0.1", 17 | "express": "^4.18.1", 18 | "fs-extra": "^10.1.0", 19 | "nodemon": "^2.0.16" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/client-side.js: -------------------------------------------------------------------------------- 1 | const api_key = "your-api-key-here" 2 | const cloud_name = "your-cloud-name-here" 3 | // It's okay for these to be public on client-side JS 4 | // You just don't ever want to leak your API Secret 5 | 6 | document.querySelector("#upload-form").addEventListener("submit", async function (e) { 7 | e.preventDefault() 8 | 9 | // get signature. In reality you could store this in localstorage or some other cache mechanism, it's good for 1 hour 10 | const signatureResponse = await axios.get("/get-signature") 11 | 12 | const data = new FormData() 13 | data.append("file", document.querySelector("#file-field").files[0]) 14 | data.append("api_key", api_key) 15 | data.append("signature", signatureResponse.data.signature) 16 | data.append("timestamp", signatureResponse.data.timestamp) 17 | 18 | const cloudinaryResponse = await axios.post(`https://api.cloudinary.com/v1_1/${cloud_name}/auto/upload`, data, { 19 | headers: { "Content-Type": "multipart/form-data" }, 20 | onUploadProgress: function (e) { 21 | console.log(e.loaded / e.total) 22 | } 23 | }) 24 | console.log(cloudinaryResponse.data) 25 | 26 | // send the image info back to our server 27 | const photoData = { 28 | public_id: cloudinaryResponse.data.public_id, 29 | version: cloudinaryResponse.data.version, 30 | signature: cloudinaryResponse.data.signature 31 | } 32 | 33 | axios.post("/do-something-with-photo", photoData) 34 | }) 35 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | const express = require("express") 3 | const cloudinary = require("cloudinary").v2 4 | const fse = require("fs-extra") 5 | const app = express() 6 | app.use(express.static("public")) 7 | app.use(express.json()) 8 | app.use(express.urlencoded({ extended: false })) 9 | 10 | const cloudinaryConfig = cloudinary.config({ 11 | cloud_name: process.env.CLOUDNAME, 12 | api_key: process.env.CLOUDAPIKEY, 13 | api_secret: process.env.CLOUDINARYSECRET, 14 | secure: true 15 | }) 16 | 17 | function passwordProtected(req, res, next) { 18 | res.set("WWW-Authenticate", "Basic realm='Cloudinary Front-end Upload'") 19 | if (req.headers.authorization == "Basic YWRtaW46YWRtaW4=") { 20 | next() 21 | } else { 22 | res.status(401).send("Try again") 23 | } 24 | } 25 | 26 | app.use(passwordProtected) 27 | 28 | app.get("/", (req, res) => { 29 | res.send(` 30 | 31 | 32 | 33 | 34 | 35 | Document 36 | 37 | 38 |

Welcome

39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | How would I use the public_id values that I store in my database? 48 | 49 | 50 | 51 | 52 | `) 53 | }) 54 | 55 | app.get("/get-signature", (req, res) => { 56 | const timestamp = Math.round(new Date().getTime() / 1000) 57 | const signature = cloudinary.utils.api_sign_request( 58 | { 59 | timestamp: timestamp 60 | }, 61 | cloudinaryConfig.api_secret 62 | ) 63 | res.json({ timestamp, signature }) 64 | }) 65 | 66 | app.post("/do-something-with-photo", async (req, res) => { 67 | // based on the public_id and the version that the (potentially malicious) user is submitting... 68 | // we can combine those values along with our SECRET key to see what we would expect the signature to be if it was innocent / valid / actually coming from Cloudinary 69 | const expectedSignature = cloudinary.utils.api_sign_request({ public_id: req.body.public_id, version: req.body.version }, cloudinaryConfig.api_secret) 70 | 71 | // We can trust the visitor's data if their signature is what we'd expect it to be... 72 | // Because without the SECRET key there's no way for someone to know what the signature should be... 73 | if (expectedSignature === req.body.signature) { 74 | // Do whatever you need to do with the public_id for the photo 75 | // Store it in a database or pass it to another service etc... 76 | await fse.ensureFile("./data.txt") 77 | const existingData = await fse.readFile("./data.txt", "utf8") 78 | await fse.outputFile("./data.txt", existingData + req.body.public_id + "\n") 79 | } 80 | }) 81 | 82 | app.get("/view-photos", async (req, res) => { 83 | await fse.ensureFile("./data.txt") 84 | const existingData = await fse.readFile("./data.txt", "utf8") 85 | res.send(`

Hello, here are a few photos...

86 | 101 |

Back to homepage

102 | `) 103 | }) 104 | 105 | app.post("/delete-photo", async (req, res) => { 106 | // do whatever you need to do in your database etc... 107 | await fse.ensureFile("./data.txt") 108 | const existingData = await fse.readFile("./data.txt", "utf8") 109 | await fse.outputFile( 110 | "./data.txt", 111 | existingData 112 | .split("\n") 113 | .filter(id => id != req.body.id) 114 | .join("\n") 115 | ) 116 | 117 | // actually delete the photo from cloudinary 118 | cloudinary.uploader.destroy(req.body.id) 119 | 120 | res.redirect("/view-photos") 121 | }) 122 | 123 | app.listen(3000) 124 | --------------------------------------------------------------------------------