hendt / ebay-api

eBay Node API in TypeScript for Node and Browser with RESTful and Traditional APIs. This library aims to implement all available eBay apis.
https://hendt.gitbook.io/ebay-api
MIT License
153 stars 40 forks source link

Help with authorization #62

Closed sunrunner4kr-simon closed 3 years ago

sunrunner4kr-simon commented 3 years ago

Hi, I'm having problems with permissions and suspect I'm doing something stupid. I don't fully understand the eBay auth tokens.

This is what I'm using:

const eBay = new eBayApi({
    appId: "######################",
    certId: "######################",
    sandbox: true,

    scope: ["https://api.ebay.com/oauth/api_scope"],

    // optional parameters, should be omitted if not used
    siteId: eBayApi.SiteId.EBAY_UK, // required for traditional APIs, see https://developer.ebay.com/DevZone/merchandising/docs/Concepts/SiteIDToGlobalID.html
    devId: "######################", // required for traditional trading API
    //ruName: "######################", // required for authorization code grant
    //authToken: "", // can be set to use traditional API without code grant
  });

  eBay.sell.fulfillment
    .getOrders()
    .then((orders) => {
      console.log(JSON.stringify(orders, null, 2));
    })
    .catch((e) => {
      console.log(e);
    });

I'm currently getting the 'Insufficient permissions to fulfill the request.' error.

I can successfully get an Auth Token from using ebay-oauth-nodejs-client

const EbayAuthToken = require("ebay-oauth-nodejs-client");
  const ebayAuthToken = new EbayAuthToken({
    filePath: path.join(configDirectory, "eBayJson.json"),
    // input file path.
  });

  (async () => {
    const token = await ebayAuthToken.getApplicationToken("SANDBOX");
    console.log(token);
  })();

Can I use this token in your API call?

Thanks in advance for the help!

dantio commented 3 years ago

Hey, you don't need to use ebay-oauth-nodejs-client. The auth is also implemented in this lib.

If you just want to try out, you can generate a token here: https://developer.ebay.com/my/auth?env=production&index=0&auth_type=oauth

(Production)

const eBay = new eBayApi({
    appId: "######################",
    certId: "######################",
    sandbox: false,

    scope: [
      'https://api.ebay.com/oauth/api_scope',
      'https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly',
      'https://api.ebay.com/oauth/api_scope/sell.fulfillment'
    ],
    siteId: eBayApi.SiteId.EBAY_UK
  });

eBay.OAuth2.setCredentials({
  refresh_token: '',
  expires_in: 7200,
  refresh_token_expires_in: 7200,
  token_type: '',
  access_token: 'v^1.1#i............................PLACE THIS LONG TOKEN HERE'
})

 eBay.sell.fulfillment
    .getOrders()
    .then((orders) => {
      console.log(JSON.stringify(orders, null, 2));
    })
    .catch((e) => {
      console.log(e);
    });

This is the way how to generate this token:

// 1. Create new eBayApi instance and set the scope.
const eBay = new eBayApi({
    appId: "######################",
    certId: "######################",
    sandbox: false,

    scope: [
      'https://api.ebay.com/oauth/api_scope',
      'https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly',
      'https://api.ebay.com/oauth/api_scope/sell.fulfillment'
    ],

    // optional parameters, should be omitted if not used
    siteId: eBayApi.SiteId.EBAY_UK
  });

const url = eBay.OAuth2.generateAuthUrl();
// 2. Open Url and Grant Access
console.log('Open URL', url);

// 3. Get the code that is placed as query parameter in redirected page
const code = 'code'; // from www.your-website?code=XXXX

// 4. Get the token
(async () => {
  // Use async/await
  const token = await eBay.OAuth2.getToken(code);
  eBay.OAuth2.setCredentials(token);

  eBay.sell.fulfillment.getOrders().then(order => {
    console.log('order', JSON.stringify(order, null, 2));
  }).catch(e => {
    console.log('error', {error: e.message});
  });
})();
sunrunner4kr-simon commented 3 years ago

Thanks for the help!

It was the 'code' part that through me off with your function. I don't understand what that is, or where I get it from?

dantio commented 3 years ago

https://developer.ebay.com/my/auth?env=production&index=0&auth_type=oauth

Here you can generate this access token. Click on the blue button "Sign in to production for OAuth" button.

This will generate this access token.

sunrunner4kr-simon commented 3 years ago

Sorry @dantio I meant this bit: -

// 3. Get the code that is placed as query parameter in redirected page
const code = 'code'; // from www.your-website?code=XXXX
dantio commented 3 years ago

eBay will redirect you to the URL you setup in your settings and add a query code to this url. Did you setup the redirect URL correctly? It must be a https site.

sunrunner4kr-simon commented 3 years ago

Do you mean from the eBay Token settings?

I'm trying to test in the sandbox first with my auth settings.

So I set up the below: -

image

sunrunner4kr-simon commented 3 years ago

Ok, so there's 2 ways to get auth. I had the credentials grant flow working with ebay-oauth-nodejs-client, whereas you're using the auth code grant flow...https://developer.ebay.com/api-docs/static/oauth-authorization-code-grant.html

I don't understand yet how to manage the redirect url etc...

So I tried passing my auth token (from the credentials flow) directly to your eBay.OAuth2.setCredentials

And now I'm passed that, I'm getting an invalid scope error: -

error {
  error: 'invalid_scope: The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client'
}

So...not sure if my token worked and there's another problem, or the scope is invalid because the token didn't work XD

sunrunner4kr-simon commented 3 years ago

OK, if I comment out the sell.fulfillment scope, I get the Access denied error...so it is probably my token.

error {
  error: 'Access denied: Insufficient permissions to fulfill the request.'
}

So, I guess .generateAuthUrl is getting the 'Grant Application Access' page. Which I'm getting: -

Open URL https://auth.sandbox.ebay.com/oauth2/authorize?client_id=SimonEdg-StoreApp-SBX-acbb713f2-79a757fb&redirect_uri=Simon_Edge-SimonEdg-StoreA-rcrfx&response_type=code&state=&scope=https%3A%2F%2Fapi.ebay.com%2Foauth%2Fapi_scope

And then a user is meant to grant consent...but there's no user involved here, it's just a process getting the code so we can swap for a token. How does the grant access bit work?

Sorry for the annoying questions...and I appreciate the help!

dantio commented 3 years ago

As I said before, you don't need to use ebay-oauth-nodejs-client. The getApplicationToken('PRODUCTION') is the same as eBay.OAuth2.getClientAccessToken(). And this is called automatically in this library (if not token is set). However, the client token does not have enough permission to give you access to fulfillments.

Let's do step by step.

  1. https://developer.ebay.com/my/auth?env=sandbox&index=0&auth_type=oauth
  2. Select Get a User Token Here
  3. Click on Sign in into Sandbox for OAuth Button
  4. Grant Acces
  5. eBay will redirect you back and show an long access_token (it already set all Scope)
  6. Copy the access_token

    eBay.OAuth2.setCredentials({
    refresh_token: '',
    expires_in: 7200,
    refresh_token_expires_in: 7200,
    token_type: '',
    access_token: 'v^1.1#i............................PLACE THIS LONG TOKEN HERE'
    })
    
    eBay.sell.fulfillment
    .getOrders()
    .then((orders) => {
      console.log(JSON.stringify(orders, null, 2));
    })

This should work!

Since this token will expire and can'tbe refreshed and you don't want to do this again, let's setup the 'redirect URL'.

Take a look in your screenshot. The 'redirect URL' is set in Your auth accepted URL. And the ruName is Simon_Edge-SimonEdg-Store-etc (you need to define this in new eBayApi(..))

So you need a working webserver that provides this endpoint (in https)! You can use something like https://ngrok.com/ to run it locally.

Now follow :


const url = eBay.OAuth2.generateAuthUrl();
// 2. Open Url and Grant Access
console.log('Open URL', url);

// 3. Get the code that is placed as query parameter in redirected page
// !!! EBAY WILL REDIRECT YOU BACK TO YOUR WEBSITE that is defined in "Your auth accepted URL"
const code = 'code'; // from www.your-website?code=XXXX

// 4. Get the token
(async () => {
  // Use async/await
  const token = await eBay.OAuth2.getToken(code);
  eBay.OAuth2.setCredentials(token);

  eBay.sell.fulfillment.getOrders().then(order => {
    console.log('order', JSON.stringify(order, null, 2));
  }).catch(e => {
    console.log('error', {error: e.message});
  });
})();```
sunrunner4kr-simon commented 3 years ago

Thanks for your patience!

So my next js app is deployed in Vercel at https://wineo.me I set my auth accepted url to that: - 'https://wineo.me'

Here's my code...with the ruName set: -

const eBay = new eBayApi({
    appId: "SimonEdg-StoreApp-xxxxxxx",
    certId: "SBX-xxxxxxx",
    sandbox: true,

    scope: [
      "https://api.ebay.com/oauth/api_scope",
      "https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly",
      "https://api.ebay.com/oauth/api_scope/sell.fulfillment",
    ],

    // optional parameters, should be omitted if not used
    siteId: eBayApi.SiteId.EBAY_UK, // required for traditional APIs, see https://developer.ebay.com/DevZone/merchandising/docs/Concepts/SiteIDToGlobalID.html
    ruName: "Simon_Edge-SimonEdg-StoreA-rcrfx", // required for authorization code grant
    //authToken: "", // can be set to use traditional API without code grant
  });

  const url = eBay.OAuth2.generateAuthUrl();
  // 2. Open Url and Grant Access
  console.log("Open URL", url);

  // 3. Get the code that is placed as query parameter in redirected page
  const code = "code"; // from www.your-website?code=XXXX

  // 4. Get the token
  (async () => {
    // Use async/await
    const token = await eBay.OAuth2.getToken(code);

    eBay.OAuth2.setCredentials(token);

    eBay.sell.fulfillment
      .getOrders()
      .then((order) => {
        console.log("order", JSON.stringify(order, null, 2));
      })
      .catch((e) => {
        console.log("error", { error: e.message });
      });
  })();

with this I get a 400 error: -

2021-04-18T10:24:12.419Z f4dd890f-0d32-4fe5-a4dc-e2752979840c ERROR Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"Error: Request failed with status code 400","reason":{"message":"Request failed with status code 400","name":"Error","stack":"Error: Request failed with status code 400\n at createError (/var/task/nextjs-store/node_modules/axios/lib/core/createError.js:16:15)\n at settle (/var/task/nextjs-store/node_modules/axios/lib/core/settle.js:17:12)\n at IncomingMessage.handleStreamEnd (/var/task/nextjs-store/node_modules/axios/lib/adapters/http.js:260:11)\n at IncomingMessage.emit (events.js:327:22)\n at endReadableNT (internal/streams/readable.js:1327:12)\n at processTicksAndRejections (internal/process/task_queues.js:80:21)","config":{"url":"https://api.sandbox.ebay.com/identity/v1/oauth2/token","method":"post","data":"grant_type=authorization_code&code=code&redirect_uri=Simon_Edge-SimonEdg-StoreA-rcrfx","headers":{"Accept":"application/json, text/plain, /","Content-Type":"application/x-www-form-urlencoded","Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"X-Requested-With, Origin, Content-Type, X-Auth-Token","Access-Control-Allow-Methods":"GET, PUT, POST, DELETE","User-Agent":"axios/0.21.1","Content-Length":85},"auth":{"username":"SimonEdg-StoreApp-SBX-xxxxxxx","password":"SBX-xxxxxxxx"},"transformRequest":[null],"transformResponse":[null],"timeout":0,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"maxBodyLength":-1}},"promise":{},"stack":["Runtime.UnhandledPromiseRejection: Error: Request failed with status code 400"," at process. (/var/runtime/index.js:35:15)"," at process.emit (events.js:327:22)"," at processPromiseRejections (internal/process/promises.js:245:33)"," at processTicksAndRejections (internal/process/task_queues.js:94:32)"]} Unknown application error occurred

So I tried your suggested test scenario and created a user token manually. And that at least works :D

The above auth request is being called from /orders so not sure if it should redirect to 'https://wineo.me/orders' but I tried setting that in case, and that doesn't work either.

dantio commented 3 years ago
  1. wrap the code that use await in try/catch
  2. use a dedicated page e.g. https://wineo.me/success for "redirect url"
  3. on this page get the query parameter code

if you use express

app.get('/success', async function(req, res) {
    // Access the provided 'page' and 'limt' query parameters
    const code = req.query.code; // this is provided from eBay
    try {
      const token = await eBay.OAuth2.getToken(code);
      eBay.OAuth2.setCredentials(token);
      const orders = await eBay.sell.fulfillment.getOrders()
     res.send(orders);
   } catch(e) {
     console.error(e)
     res.sendStatus(400)
  }
});
sunrunner4kr-simon commented 3 years ago

Man I'm confused...

So my /orders defines the eBayApi entity and generates the auth url, storing in eBayApi object (this is where I want the orders data)

When visiting that url, ebay redirects to /success with a code, which /success uses to get the token and with that gets the orders.

I don't use express :/ So had a stab with the below

export default async (req, res) => {
  // Access the provided 'page' and 'limt' query parameters
  const code = req.query.code; // this is provided from eBay
  try {
    const token = await eBay.OAuth2.getToken(code);
    eBay.OAuth2.setCredentials(token);
    const orders = await eBay.sell.fulfillment.getOrders();
    res.send(orders);
  } catch (e) {
    console.error(e);
    res.sendStatus(400);
  }
};

But where am I calling the auth url? does it visit the url in generateAuthUrl ? And if it does, where do the results of visiting /success get stored ?

dantio commented 3 years ago

Agree, It is confusing :)

So this would be flow example:

  1. User goes to https://wineo.me/login
  2. On this page you provide a "eBay Login Button" -> const url = eBay.OAuth2.generateAuthUrl(); <a href="${url}">Login</a>
  3. User clicks on it, he will be redirected to eBay for Login
  4. User click on "Accept the wineo App" Button on this page
  5. He get's redirected back to https://wineo.me/success?code=SECRET_CODE
  6. Your app get's the code and calls const token = await eBay.OAuth2.getToken(code);
  7. Now you can save this token in e.g. the session
  8. User visits https://wineo.me/orders
  9. Your app get's the token from the session and calls eBay.OAuth2.setCredentials(token);
  10. You can now call the getOrders() method :)

I hope it helps

What do you use for routing?

sunrunner4kr-simon commented 3 years ago

Thanks!

Ah, I think this is where my problem is. I don’t want a user to log in to get their orders. What I need my app to do, is get the orders for a shop. So i wanted to set up a process in the app to get’s the orders and inventory and I was going to use the notifications API to get notifications of when new orders come in. Which is why i originally tried the Client Credentials flow...although I’ve now learnt that doesn’t work for the Fulfilment API.

So for me, the user would need to be the shop owner. But I wouldn’t want them to have to log in to eBay each time they use the app. And it wouldn’t work then when any other user logs in to the app (works in the shop) but doesn’t have the eBay shop user account.

Ideally I’d want a way to get this data in the background. But that doesn’t seem possible with the auth code flow.

So far I’ve only really had to use next/link to get around

dantio commented 3 years ago

The user only needs to login once you can store the token in db. And if you have the token you can fetch the orders in the background.

If you plan to have multiple ebay user, you have to use this way because every ebay user needs to authorize your application. After 2 hours the token expires and it will be refreshed (automatically) this new token needs to stored in db.

If you only want to do it for a single shop, you could use the AuthNauth token and the old traditional api. It also has getorders and the token is valid for 2 years or so.

sunrunner4kr-simon commented 3 years ago

Hmm..ok. The app will have multiple users, but local to the app. It’s only one eBay shop account that needs authorising. I might make a link in a settings page for that one user to log in to eBay to grant access. Then in the background it can be getting the data and refreshing automatically, without them ever having to do it again. And if something does go wrong, they can just log in and grant access again from the settings.

I’ll only pull the data when they visit /orders so the token will mostly likely going expire a lot of the time. So when it tries again, and gets the ‘invalid access token’ error, it will automatically refresh? How does that work, do I need to catch the error in the getOrders call? Or will it hit /success url redirect with a new code?

dantio commented 3 years ago

Yes that's sound good!

eBay.OAuth2.on('refreshAuthToken', (token) => { console.log(token) // Here you store this new token on database });

sunrunner4kr-simon commented 3 years ago

Great! Thanks for all the help. I’ll give it a go now :)

sunrunner4kr-simon commented 3 years ago

I'm still trying to test the above, but I have an unrelated prisma.io bug. Had to raise a bug report :( https://github.com/prisma/prisma/issues/6730

sunrunner4kr-simon commented 3 years ago

Working like a charm!! Thanks for all the help

workteam123 commented 3 years ago

Hi @sunrunner4kr-simon - and thanks @dantio for the lib. I want my express app to communicate with ebay. I have generated a user token at https://developer.ebay.com/my/auth/?env=sandbox&index=0&auth_type=oauth and have used this in as ebayAuthKey from Get a User Token Here

ebay.OAuth2.setCredentials({ refresh_token: '', expires_in: 7200, refresh_token_expires_in: 7200, token_type: '', access_token: ebayAuthKey })

How are the credentials updated i.e. can you use ebay.OAuth2.on('refreshAuthToken', (token) => { console.log(token) // Here you store this new token on database // add refresh token here? });

@sunrunner4kr-simon - can you share your flow please

Many thanks Gary

sunrunner4kr-simon commented 3 years ago

I use Prisma Gary, so if my request fails and hits the catch i refresh the token and store in my db

const token = await prisma.variable.findFirst({
    where: {
      variable: "ebayToken",
    },
  });

if (token) {
    eBay.OAuth2.setCredentials(token.token);
    eBay.sell.fulfillment
      .getOrders()
      .then((order) => {
        console.log("order", JSON.stringify(order, null, 2));
      })
      .catch((e) => {
        console.log("error", { error: e.message });
        //const error_message = { error: e.message };
        eBay.OAuth2.on("refreshAuthToken", (token) => {
          console.log("Refresh token:", token);

          async () => {
            const result = await prisma.env_variable.upsert({
              where: {
                variable: "ebayToken",
              },
              update: { token: token },
              create: {
                variable: "ebayToken",
                token: token,
              },
            });
            if (result) {
              console.log("Refresh Token Stored in DB");
            } else console.log("Failed to store refresh Token");
          };
        })();
      });
  }

my DB token field is JSON

I haven't been running it long enough to see if the token is refreshed, but I'm now succesfully getting responses from ebay

sunrunner4kr-simon commented 3 years ago

OK...so the token has timed out, and I get an error in my request (as expected)

error {
  error: 'invalid_scope: The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client'
}

But it's not refreshing the token, in fact i don't know if it's doing anything. from here onwards as it's not logging any of my console logs from the refreshfunction onwards. Like it's skipping the whole of the below.

eBay.OAuth2.on("refreshAuthToken", (token) => {
              console.log("Refresh token:", token);

              async () => {
                const result = await prisma.env_variable.upsert({
                  where: {
                    variable: "ebayToken",
                  },
                  update: { token: token },
                  create: {
                    variable: "ebayToken",
                    token: token,
                  },
                });
                if (result) {
                  console.log("Refresh Token Stored in DB");
                } else console.log("Failed to store refresh Token");
              };
            })();

If I manually request auth again, then I can get a new token.

sunrunner4kr-simon commented 3 years ago

Sorry @dantio so close to getting this working.

I now have it calling the refreshAuthToken function in the catch, but it's not getting a valid token.

For example, here is my code: -

.catch((e) => {
            console.log("error", { error: e.message });
            //const error_message = { error: e.message };
            eBay.OAuth2.on("refreshAuthToken", (token) => {
              console.log("Refresh token:", token);
              updateToken(token);
            });
          })();

that console log isn't called...which I don't understand, but it does call the updateToken function: -

async function updateToken(token) {
    console.log("Refresh token:", token);
    let response = await prisma.env_variable.upsert({
      where: {
        variable: "ebayToken",
      },
      update: { token: token },
      create: {
        variable: "ebayToken",
        token: token,
      },
    });

    if (response) {
      console.log("Token Refreshed: ", response);
      //throw new Error(`Prisma Error: ${response}`);
    }
  }

  updateToken().catch((e) => {
    console.error("updateToken" + e.message);
  });

but token isn't defined. So my question is, eBay.OAuth2.on "refreshAuthToken" stores the new token in the (token) variable right? But it's coming back undefined

In your readme, it says it will refresh when we get a 'invalid access token' error, but I'm getting the below error when it expires: -

'invalid_scope: The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client'

dantio commented 3 years ago
eBay.OAuth2.on("refreshAuthToken", (token) => {
              console.log("Refresh token:", token);
              updateToken(token);
            });

Should not be used in catch block instead move it before you call any api method.

eBay.OAuth2.setCredentials(token.token);
eBay.OAuth2.on("refreshAuthToken", (token) => {
              console.log("Refresh token:", token);
              updateToken(token);
});

// if the token is expired, it will be refreshed and 'refreshAuthToken' is fired
eBay.sell.fulfillment.getOrders().then((order) => {
        console.log("order", JSON.stringify(order, null, 2));
})
// if you put it in catch block, it's already too late
sunrunner4kr-simon commented 3 years ago

Hmmm...so it also annoyingly gives an error, even when the request is successful. For example I call SetNotificationPreferences in the Trading API and get a successfully response (in fact i get a successful respone twice :/ ): -

Set Pref {
  Timestamp: '2021-04-28T10:00:45.539Z',
  Ack: 'Success',
  Version: 1173,
  Build: 'E1173_CORE_APINOTIFY_19146596_R1'
}
Set Pref {
  Timestamp: '2021-04-28T10:00:45.646Z',
  Ack: 'Success',
  Version: 1173,
  Build: 'E1173_CORE_APINOTIFY_19146596_R1'
}

But it falls in to the catch with the error: -

TypeError: eBay.trading.SetNotificationPreferences(...).then(...).catch(...) is not a function
    at exports.modules../pages/api/ebay/getPreferences.js.__webpack_exports__.default (D:\Web\StoreApp\nextjs-store\.next\server\pages\api\ebay\getPreferences.js:209:11)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:94:5)
    at async apiResolver (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\api-utils.js:8:1)
    at async DevServer.handleApiRequest (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\next-server.js:67:462)       
    at async Object.fn (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\next-server.js:59:492)
    at async Router.execute (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\router.js:25:67)
    at async DevServer.run (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\next-server.js:69:1042)
    at async DevServer.handleRequest (D:\Web\StoreApp\nextjs-store\node_modules\next\dist\next-server\server\next-server.js:34:504)

I've moved everything in to it's own async function: -

async function setPreferences(itemListed, auctionCheckoutComplete) {
    eBay.OAuth2.setCredentials(token.token);
    eBay.OAuth2.on("refreshAuthToken", (token) => {
      console.log("Refresh token:", token);
      updateToken(token);
    });
    let response = await eBay.trading.SetNotificationPreferences({
      UserDeliveryPreferenceArray: {
        NotificationEnable: {
          EventType: "AuctionCheckoutComplete",
          EventEnable: auctionCheckoutComplete,
        },
        NotificationEnable: {
          EventType: "ItemListed",
          EventEnable: itemListed,
        },
      },
    });

    if (response) {
      console.log("Set Pref", response);
    }
  }

  setPreferences().catch((e) => {
    consol
e.log("error", { error: e.message });
  });

token - return by refreshAuthToken is still undefined

Refresh token: undefined

updateToken Cannot read property 'upsert' of undefined
sunrunner4kr-simon commented 3 years ago

ah! will refreshAuthToken return null if it didn't need refreshing?

dantio commented 3 years ago

Hm can you update the lib to latest one v3.2.0.

refreshAuthToken should not be triggered if token is not refreshed...

Please avoid mixing Promise (then) and async/await.

function setPreferences(itemListed, auctionCheckoutComplete) {
    eBay.OAuth2.setCredentials(token.token);
    eBay.OAuth2.on("refreshAuthToken", (token) => {
        console.log("Refresh token:", token);
        updateToken(token);
    });
    return eBay.trading.SetNotificationPreferences({
      UserDeliveryPreferenceArray: {
        NotificationEnable: {
          EventType: "AuctionCheckoutComplete",
          EventEnable: auctionCheckoutComplete,
        },
        NotificationEnable: {
          EventType: "ItemListed",
          EventEnable: itemListed,
        },
      },
    });
}

setPreferences().then((response) => {
    if (response) {
      console.log("Set Pref", response);
    }
})
.catch((e) => {
    console.log("error", { error: e.message });
  });
sunrunner4kr-simon commented 3 years ago

:/ now i'm not even getting the successful response

In my API i'm calling the above setPreferences function: -

case "PUT":
      const itemListed = req.body.itemListed;
      const auctionCheckoutComplete = req.body.auctionCheckoutComplete;
      console.log(
        "NOTIFICATION API: Processing PUT - ",
        itemListed,
        " : ",
        auctionCheckoutComplete
      );

      setPreferences(itemListed, auctionCheckoutComplete);

      res.status(200).end();
      break;

with the function as you suggested: -

function setPreferences(itemListed, auctionCheckoutComplete) {
    eBay.OAuth2.setCredentials(token.token);
    eBay.OAuth2.on("refreshAuthToken", (token) => {
      console.log("Refresh token:", token);
      updateToken(token);
    });
    return eBay.trading.SetNotificationPreferences({
      UserDeliveryPreferenceArray: {
        NotificationEnable: {
          EventType: "AuctionCheckoutComplete",
          EventEnable: auctionCheckoutComplete,
        },
        NotificationEnable: {
          EventType: "ItemListed",
          EventEnable: itemListed,
        },
      },
    });
  }

  setPreferences()
    .then((response) => {
      if (response) {
        console.log("Set Pref", response);
      }
    })
    .catch((e) => {
      console.log("error", { error: e.message });
    });

I'm no longer getting any console logging from the API or the function .then or any catch errors...

however it IS still trying to refresh the token

Refresh token: undefined

And i'm getting an internal server error from my api request

dantio commented 3 years ago
case "PUT":
      const itemListed = req.body.itemListed;
      const auctionCheckoutComplete = req.body.auctionCheckoutComplete;
      console.log(
        "NOTIFICATION API: Processing PUT - ",
        itemListed,
        " : ",
        auctionCheckoutComplete
      );
      // this is promise. You need to call .then
      setPreferences(itemListed, auctionCheckoutComplete).then((response) => {
          res.send(response);
      }).catch((error) => {
        res.sendStatus(500); // or whatever
        console.error(error)
      })
      break;

and remove:

 setPreferences()
    .then((response) => {
      if (response) {
        console.log("Set Pref", response);
      }
    })
    .catch((e) => {
      console.log("error", { error: e.message });
    });

you already calling it in the PUT case.

Mayve you use multiple eBay.OAuth2.on("refreshAuthToken", (token) => {}) ?

I can't explaing why it's logging undefined.. It's not even possible since it's always an object or an error is thrown. Did you updated to latest lib version?

dantio commented 3 years ago

Can you run your App with this env variable: DEBUG=ebay:* node your-app.js

sunrunner4kr-simon commented 3 years ago

Not that I'm aware of...

I have a settings page with a button that toggles the preference and triggers sending the SetNotification

                  <Button
                    variant="contained"
                    color="secondary"
                    onClick={() => togglePreference("itemListed")}
                  >
                    <a>{state.itemListed ? "Turn Off" : "Turn On"}</a>
                  </Button>
function togglePreference(pref) {
    var itemListed = state.itemListed === true ? "Enable" : "Disable";
    var auctionCheckoutComplete =
      state.itemListed === true ? "Enable" : "Enable";
    if (pref === "auctionCheckoutComplete") {
      auctionCheckoutComplete =
        state.auctionCheckoutComplete === true ? "Disable" : "Enable";
    } else if (pref === "itemListed") {
      itemListed = state.itemListed === true ? "Disable" : "Enable";
    }
    setPreferences(itemListed, auctionCheckoutComplete)
      .then((response) => {
        if (response.status === 200) {
          console.log("Toggle successful");
        } else if (response.status === 500) {
          console.log("Toggle unsuccessful");
        }
      })
      .catch((error) => {
        console.error(error);
      });
  }

which calls: -

function setPreferences(itemListed, auctionCheckoutComplete) {
    return fetch(`${server}/api/ebay/getPreferences`, {
      method: "put",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        auctionCheckoutComplete: auctionCheckoutComplete,
        itemListed: itemListed,
      }),
    });
  }

Which then hits the above PUT method of the API: -

case "PUT":
      const itemListed = req.body.itemListed;
      const auctionCheckoutComplete = req.body.auctionCheckoutComplete;
      console.log(
        "NOTIFICATION API: Processing PUT - ",
        itemListed,
        " : ",
        auctionCheckoutComplete
      );

      setPreferences(itemListed, auctionCheckoutComplete)
        .then((response) => {
          res.send(response);
        })
        .catch((error) => {
          res.sendStatus(500);
          console.log(error);
        });

      break;

I also have a GET method in that API switch that currently calls refreshAuthToken. But for the sake of testing i've commented that all out, so there shouldn't be anything calling it.

With the above I get the 500 from the PUT: -

image

And to the server console, I can see it's trying the refresh: -

Refresh token: undefined

What do you mean sorry by 'your-app.js' ?

dantio commented 3 years ago

Do you know how you set environment variables in your app?

What error is displayed on the server side?

sunrunner4kr-simon commented 3 years ago

I've got an env.local file I can put it in, just not sure what 'your-app' would be. Is this the '_app.js'?

image

dantio commented 3 years ago

Are you calling this

updateToken().catch((e) => {
    console.error("updateToken" + e.message);
  });

somewhere in your app? Remove it.

As you can see the error is not only from the eBay API itself. prisma.env_variableis undefined. Where do you store the token env_variable or variable ??

function setPreferences(itemListed, auctionCheckoutComplete) {
  const token = await prisma.variable.findFirst({
      where: {
        variable: "ebayToken",
    }
  });
  eBay.OAuth2.setCredentials(token.token);
  eBay.OAuth2.on("refreshAuthToken", (token) => {
    const result = await prisma.variable.upsert({
              where: {
                variable: "ebayToken",
              },
              update: { token: token },
              create: {
                variable: "ebayToken",
                token: token,
              },
            });
  });

  return eBay.trading.SetNotificationPreferences({
      UserDeliveryPreferenceArray: {
        NotificationEnable: {
          EventType: "AuctionCheckoutComplete",
          EventEnable: auctionCheckoutComplete,
        },
        NotificationEnable: {
          EventType: "ItemListed",
          EventEnable: itemListed,
        },
      },
    });
  }
sunrunner4kr-simon commented 3 years ago

the env_variable was a typo :(

but I can't put the prisma await within the function because it needs to be an async function. Which is why I put the prisma DB update in it's own async function, like that updateToken() and why I was getting the token from the DB outside of my setPreferences function

sunrunner4kr-simon commented 3 years ago

Ok, this works, when I have a valid token: -

case "PUT":
      const itemListed = req.body.itemListed;
      const auctionCheckoutComplete = req.body.auctionCheckoutComplete;
      console.log(
        "NOTIFICATION API: Processing PUT - ",
        itemListed,
        " : ",
        auctionCheckoutComplete
      );

      setPreferences(itemListed, auctionCheckoutComplete)
        .then((response) => {
          res.status(200);
          console.log(response);
          res.send(response);
        })
        .catch((error) => {
          res.status(500);
          console.log(error);
        });

      break;
const token = await prisma.variable.findFirst({
    where: { variable: "ebayToken" },
  });

    async function updateToken(token) {
    console.log("Refresh token:", token);
    let response = await prisma.variable.upsert({
      where: {
        variable: "ebayToken",
      },
      update: { token: token },
      create: {
        variable: "ebayToken",
        token: token,
      },
    });

    if (response) {
      console.log("Token Refreshed: ", response);
    }
  }

  function setPreferences(itemListed, auctionCheckoutComplete) {
    eBay.OAuth2.setCredentials(token.token);
    eBay.OAuth2.on("refreshAuthToken", (token) => {
      updateToken(token).catch((e) => {
        console.error("Update Token failed: ", e.message);
      });
    });
    return eBay.trading.SetNotificationPreferences({
      UserDeliveryPreferenceArray: {
        NotificationEnable: {
          EventType: "AuctionCheckoutComplete",
          EventEnable: auctionCheckoutComplete,
        },
        NotificationEnable: {
          EventType: "ItemListed",
          EventEnable: itemListed,
        },
      },
    });
  }
dantio commented 3 years ago

It looks good to me. So did work now?

sunrunner4kr-simon commented 3 years ago

It worked with a valid token. I waited for the token to timeout and reloaded the page. Like before I get the 'invalid scope' error: -

NOTIFICATION API: Processing PUT -  Disable  :  Enable
API resolved without sending a response for /api/ebay/getPreferences, this may result in stalled requests.
EBayInvalidScope: invalid_scope: The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client
    at EBayInvalidScope.EBayError [as constructor] (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:45:28)     
    at EBayInvalidScope.EbayApiError [as constructor] (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:122:24) 
    at new EBayInvalidScope (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:182:42)
    at Object.exports.handleEBayError (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:243:15)
    at Traditional.<anonymous> (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:240:34)
    at step (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:76:23)
    at Object.throw (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:57:53)
    at rejected (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:49:65)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:94:5) {
  meta: {
    message: 'invalid_scope',
    description: 'The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client',
    res: { status: 400, statusText: 'Bad Request', headers: [Object] },
    req: {
      url: 'https://api.sandbox.ebay.com/identity/v1/oauth2/token',
      method: 'post',
      headers: [Object],
      params: undefined
    },
    [Symbol(raw-error)]: Error: Request failed with status code 400
        at createError (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\core\createError.js:16:15)
        at settle (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\core\settle.js:17:12)
        at IncomingMessage.handleStreamEnd (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\adapters\http.js:260:11)
        at IncomingMessage.emit (node:events:391:22)
        at endReadableNT (node:internal/streams/readable:1307:12)
        at processTicksAndRejections (node:internal/process/task_queues:81:21) {
      config: [Object],
      request: [ClientRequest],
      response: [Object],
      isAxiosError: true,
      toJSON: [Function: toJSON]
    }
  }
}

So I'm assuming it doesn't refresh the token. It definitely isn't calling updateToken

I need to check out that API error: -

API resolved without sending a response for /api/ebay/getPreferences, this may result in stalled requests.

dantio commented 3 years ago

Would it be possible to share your whole git repo so I can reproduce it?

sunrunner4kr-simon commented 3 years ago

Yeah of course, it's all in github, i'll invite you

sunrunner4kr-simon commented 3 years ago

I need to update the GET case with the correct promise from the PUT we've fixed. But that's not what i'm logging the error from.

sunrunner4kr-simon commented 3 years ago

After the merge...I'm afraid :(

NOTIFICATION API: Processing PUT -  Disable  :  Enable
API resolved without sending a response for /api/ebay/getPreferences, this may result in stalled requests.
EBayInvalidScope: invalid_scope: The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client
    at EBayInvalidScope.EBayError [as constructor] (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:45:28)     
    at EBayInvalidScope.EbayApiError [as constructor] (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:122:24) 
    at new EBayInvalidScope (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:182:42)
    at Object.exports.handleEBayError (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\errors\index.js:243:15)
    at Traditional.<anonymous> (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:240:34)
    at step (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:76:23)
    at Object.throw (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:57:53)
    at rejected (D:\Web\StoreApp\nextjs-store\node_modules\@hendt\ebay-api\lib\api\traditional\index.js:49:65)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:94:5) {
  meta: {
    message: 'invalid_scope',
    description: 'The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client',
    res: { status: 400, statusText: 'Bad Request', headers: [Object] },
    req: {
      url: 'https://api.sandbox.ebay.com/identity/v1/oauth2/token',
      method: 'post',
      headers: [Object],
      params: undefined
    },
    [Symbol(raw-error)]: Error: Request failed with status code 400
        at createError (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\core\createError.js:16:15)
        at settle (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\core\settle.js:17:12)
        at IncomingMessage.handleStreamEnd (D:\Web\StoreApp\nextjs-store\node_modules\axios\lib\adapters\http.js:260:11)
        at IncomingMessage.emit (node:events:391:22)
        at endReadableNT (node:internal/streams/readable:1307:12)
        at processTicksAndRejections (node:internal/process/task_queues:81:21) {
      config: [Object],
      request: [ClientRequest],
      response: [Object],
      isAxiosError: true,
      toJSON: [Function: toJSON]
    }
  }
}
dantio commented 3 years ago

I think I know the problem :) you are missing the trading scopes. You use it in getPreference but not in the success.js and it's requuered where you generate the ebay auth url. Keep all scopes identically, delete the token from db and do the auth flow again.

sunrunner4kr-simon commented 3 years ago

aaahhh damn XD

Well, I've got this scope everywhere now. Do you know where I can find a list of valid scopes? I'm getting invalid scope response for these

scope: [ "https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly", "https://api.ebay.com/oauth/api_scope/sell.fulfillment", "https://api.ebay.com/oauth/api_scope/trading.readonly", "https://api.ebay.com/oauth/api_scope/trading", ],

I can only find this https://developer.ebay.com/api-docs/static/oauth-scopes.html#scopes-and-flows Which explains how to use scopes, but not a definition of each one

dantio commented 3 years ago

Well, these are the scopes what I'm using. But it would great to have it in the docs...

scope: [
          'https://api.ebay.com/oauth/api_scope',
          'https://api.ebay.com/oauth/api_scope/sell.marketing.readonly',
          'https://api.ebay.com/oauth/api_scope/sell.marketing',
          'https://api.ebay.com/oauth/api_scope/sell.inventory.readonly',
          'https://api.ebay.com/oauth/api_scope/sell.inventory',
          'https://api.ebay.com/oauth/api_scope/sell.account.readonly',
          'https://api.ebay.com/oauth/api_scope/sell.account',
          'https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly',
          'https://api.ebay.com/oauth/api_scope/sell.fulfillment',
          'https://api.ebay.com/oauth/api_scope/sell.analytics.readonly',
          'https://api.ebay.com/oauth/api_scope/sell.finances',
          'https://api.ebay.com/oauth/api_scope/sell.payment.dispute',
          'https://api.ebay.com/oauth/api_scope/commerce.identity.readonly'
        ]
sunrunner4kr-simon commented 3 years ago

Found this: - https://developer.ebay.com/api-docs/static/oauth-trad-apis.html#OAuth

So I tried using these based on their recommendation of what to use

image

However, I was getting invalid scope error still.

Tried your list, and it works :D

So, all working again....will wait for the token to timeout again, and see if the refresh works!

sunrunner4kr-simon commented 3 years ago

It worked!!! Thank you so much @dantio for all your help