moto
moto

Reputation: 940

Cloudinary Signed Uploads with Widget

Documentation is extremely frustrating.

I'm using the upload widget to try to allow users to upload multiple pictures for their profile. I can't use unsigned uploads because of the potential for abuse.

I would much rather upload the file through the upload widget instead of through the server as it seems like it should be so simple

I've pieced together what I think should work but it is still saying: Upload preset must be whitelisted for unsigned uploads

Server:

// grab a current UNIX timestamp
const millisecondsToSeconds = 1000;
const timestamp = Math.round(Date.now() / millisecondsToSeconds);
// generate the signature using the current timestmap and any other desired Cloudinary params
const signature = cloudinaryV2.utils.api_sign_request({ timestamp }, CLOUDINARY_SECRET_KEY);
// craft a signature payload to send to the client (timestamp and signature required)
return signature;

also tried

return {
  signature,
  timestamp,
};

also tried

const signature = cloudinaryV2.utils.api_sign_request(
      data.params_to_sign,
      CLOUDINARY_SECRET_KEY,
    );

Client:

const generateSignature = async (callback: Function, params_to_sign: object): Promise<void> => {
  try {
    const signature = await generateSignatureCF({ slug: 'xxxx' }); 
  // also tried { slug: 'xxxx', params_to_sign }
    callback(signature);
  } catch (err) {
    console.log(err);
  }
};
cloudinary.openUploadWidget(
  {
    cloudName: 'xxx',
    uploadPreset: 'xxxx',
    sources: ['local', 'url', 'facebook', 'dropbox', 'google_photos'],
    folder: 'xxxx',
    apiKey: ENV.CLOUDINARY_PUBLIC_KEY,
    uploadSignature: generateSignature,
  },
  function(error, result) {
    console.log(error);
  },
);

Upvotes: 1

Views: 1100

Answers (2)

Deminetix
Deminetix

Reputation: 2974

Let's all take a moment to point out how horrible Cloudinary's documentation is. It's easily the worst i've ever seen. Nightmare fuel.

Now that i've got that off my chest... I really needed to be able to do this and I spent way too long banging my head against walls for what should be extremely simple. Here it is...

Server (Node.js)

You'll need an endpoint that returns a signature-timestamp pair to the frontend:

import cloudinary from 'cloudinary'

export async function createImageUpload() {
  const timestamp = new Date().getTime()
  const signature = await cloudinary.utils.api_sign_request(
    {
      timestamp,
    },
    process.env.CLOUDINARY_SECRET
  )
  return { timestamp, signature }
}

Client (Browser)

The client makes a request to the server for a signature-timestamp pair and then uses that to upload a file. The file used in the example should come from an <input type='file'/> change event etc.

const CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME
const API_KEY = process.env.CLOUDINARY_API_KEY

async function uploadImage(file) {
  const { signature, timestamp } = await api.post('/image-upload')
  const form = new FormData()
  form.append('file', file)
  const res = await fetch(
    `https://api.cloudinary.com/v1_1/${CLOUD_NAME}/image/upload?api_key=${API_KEY}&timestamp=${timestamp}&signature=${signature}`,
    {
      method: 'POST',
      body: form,
    }
  )
  const data = await res.json()
  return data.secure_url
}

That's it. That's all it takes. If only Cloudinary had this in their docs.

Upvotes: 7

moto
moto

Reputation: 940

Man. I hate my life. I finally figured it out. It literally took me beautifying the upload widget js to understand that the return of the function should be a string instead of an object even though the docs make it seem otherwise.

Here is how to implement a signed upload with a Firebase Cloud Function

import * as functions from 'firebase-functions';
import cloudinary from 'cloudinary';
const CLOUDINARY_SECRET_KEY = functions.config().cloudinary.key;
const cloudinaryV2 = cloudinary.v2;
module.exports.main = functions.https.onCall(async (data, context: CallableContext) => {
  // Checking that the user is authenticated.
  if (!context.auth) {
    // Throwing an HttpsError so that the client gets the error details.
    throw new functions.https.HttpsError(
      'failed-precondition',
      'The function must be called while authenticated.',
    );
  }
  try {
    return cloudinaryV2.utils.api_sign_request(data.params_to_sign, CLOUDINARY_SECRET_KEY);
  } catch (error) {
    throw new functions.https.HttpsError('failed-precondition', error.message);
  }
});


// CLIENT
const uploadWidget = () => {
  const generateSignature = async (callback: Function, params_to_sign: object): Promise<void> => {
    try {
      const signature = await generateImageUploadSignatureCF({ params_to_sign });
      callback(signature.data);
    } catch (err) {
      console.log(err);
    }
  };

  cloudinary.openUploadWidget(
    {
      cloudName: 'xxxxxx',
      uploadSignature: generateSignature,
      apiKey: ENV.CLOUDINARY_PUBLIC_KEY,
    },
    function(error, result) {
      console.log(error);
    },
  );
};

Upvotes: 1

Related Questions