Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORS error when using this library in web app #9

Open
chrissyast opened this issue Mar 5, 2022 · 4 comments
Open

CORS error when using this library in web app #9

chrissyast opened this issue Mar 5, 2022 · 4 comments

Comments

@chrissyast
Copy link

I have a nuxt app which throws an error when trying to make requests through the TuyaContext.

Access to XMLHttpRequest at 'https://openapi.tuyaeu.com/v1.0/token?grant_type=1' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'https://iot.tuya.com' that is not equal to the supplied origin.

@monecchi
Copy link

monecchi commented May 21, 2022

For anyone coming around the issue, the following assumes you've already created an account on Tuya IoT Platform as also a Cloud Project in order to get the required credentials, aka Client ID & Client Secret, and the project API endpoint (Data center url address).

I was facing the same issue regarding CORS policy, but I've solved that by abstracting tuya-connector-nodejs in order to create an ordinary nodejs / express server. Also I've used cors package to configure the Access-Control-Allow-Origin CORS header... I'm sharing the simplified Custom API set up with its own routes which then call tuya-connector-nodejs functions.

On Tuya Iot Platform -> Cloud -> Development: I've left the Cloud Authorization IP Allowlist option disabled, as my intention was to run the server on localhost only.

Nodejs / Express:

.env

TUYA_DATA_CENTER_URL=https://openapi.tuyaus.com
TUYA_CLIENT_ID=
TUYA_CLIENT_SECRET=
TUYA_IMAGE_URL=https://images.tuyacn.com/

src/config.js

import dotenv from 'dotenv';
dotenv.config()

// Tuya Cloud - IoT Platform
const config = {
  baseUrl: `${process.env.TUYA_DATA_CENTER_URL}`, // Western America Data Center
  clientId: `${process.env.TUYA_CLIENT_ID}`, // Access ID / Client ID
  clientSecret: `${process.env.TUYA_CLIENT_SECRET}`, // Access Secret / Client Secret:
  apiImageUrl: `${process.env.TUYA_IMAGE_URL}` // https://images.tuyacn.com/
};

if (!Object.values(config).every(Boolean)) {
  throw new Error(
    'Please create .env from .env.example and specify all values',
  );
}

export default config;

src/index.js

import dotenv from 'dotenv'
import express from 'express'
import axios from 'axios'
import cors from 'cors'

// Custom Tuya Api Routes
import { routes } from './routes/index.js'

const router = express.Router()

dotenv.config()
const port = 3399
const app = express()

app.use(express.urlencoded({ extended: true }))
app.use(express.json({ limit: '32mb' }))

app.use(cors({ origin: '*' })) // This alone didn't work!
app.use(
  cors({
    origin: [
      'https://openapi.tuyeu.com',
      'https://openapi.tuyaus.com',
      'http://localhost:3000',
      'http://localhost:3399',
    ],
    methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'], 
    allowedHeaders: [
      'Origin',
      'X-Requested-With',
      'Content-Type',
      'Accept',
      'Signature-Headers',
      'client_id',
      'access_token',
      'sign',
      'sign_method',
      'stringToSign',
      't',
      'nonce',
    ], 
  })
)

app.use(routes)

app.listen(port, () => {
    console.log(
      `TuyaApi Started ✓ - Express listening on port : ${port} - press Ctrl-C to terminate.`
    )
})

src/routes/index.js

import express from 'express'
const router = express.Router()

// Main Api Routes
import { apiRoutes } from './api/index.js' // src/routes/api/index.js

const api = '/api/v1' 

router.use(api, apiRoutes)
router.use(api, (req, res) => res.status(404).json('No API route found'))

export { router as routes }

src/routes/api/index.js

import express from 'express'
const router = express.Router()

import { deviceRoutes } from './devices.js' // src/routes/api/device.js
import { userRoutes } from './user.js' // src/routes/api/user.js

// Device Routes
router.use('/device', deviceRoutes)

// User Routes
router.use('/users', userRoutes)

// Other Routes...

export { router as apiRoutes }

src/utils/redisConnect.js

// Redis (ioRedis) connection
import dotenv from 'dotenv'
dotenv.config()

export const redisConnect = {
  port: `${process.env.REDIS_PORT}`,
  host: `${process.env.REDIS_HOST}`,
  username: 'default',
  password: `${process.env.REDIS_PASS}`,
  db: 0
}

src/utils/tokenStore.js

//
// redis store
// @see: https://github.com/tuya/tuya-connector-nodejs
//
import Redis from 'ioredis'
import { redisConnect } from '../utils/redisConnect.js'

export class RedisTokenStore {
  client = new Redis(redisConnect)

  constructor (client, key = 'tuya::token') {
    this.client = client
    this.key = key
  }

  async setTokens (tokens) {
    const res = await this.client.set(this.key, JSON.stringify(tokens))
    return !!res
  }

  async getAccessToken () {
    const jsonStr = (await this.client.get(this.key)) || '{}'
    const tokens = JSON.parse(jsonStr)
    return tokens && tokens.access_token
  }

  async getRefreshToken () {
    const jsonStr = (await this.client.get(this.key)) || '{}'
    const tokens = JSON.parse(jsonStr)
    return tokens.refresh_token
  }
}

src/routes/api/device.js

Here tuya-connector-nodejs functions are wrapped within ordinary express router logic...

import express from 'express'
import axios from 'axios'
import Redis from 'ioredis'

import { TuyaContext } from '@tuya/tuya-connector-nodejs'
import { RedisTokenStore } from '../../utils/tokenStore.js'
import { redisConnect } from '../../utils/redisConnect.js'

import config from '../../config.js'
const { baseUrl, clientId, clientSecret, deviceId, uid } = config

const router = express.Router()

const redis = new Redis(redisConnect)

const tuya = new TuyaContext({
  baseUrl: baseUrl, // https://openapi.tuyaus.com',
  accessKey: clientId,
  secretKey: clientSecret,
  store: new RedisTokenStore(redis),
  rpc: axios
})

//
// Devices API Routes
// Tuya Cloud API Reference: https://developer.tuya.com/en/docs/iot/api-reference?id=Ka7qb7vhber64
// @see: https://github.com/tuya/tuya-connector-nodejs
//

//
// Post commands to device
//
router.post('/:id/commands', async (req, res) => {
  try {
    const deviceId = req.params.id
    const { commands } = req.body

    const result = await tuya.request({
      method: 'POST',
      path: `/v1.0/devices/${deviceId}/commands`,
      body: { commands }
    })

    res.status(200).json({
      ...result,
      message: 'Device updated successfully!'
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

//
//
// Update (modify) device details, such as name etc..
//
router.put('/:id/update', async (req, res) => {
  try {
    const deviceId = req.params.id
    const update = req.body

    if (Object.keys(update).length === 0) {
      return res.status(400).json({ error: 'Missing body params' })
    }

    const result = await tuya.request({
      method: 'PUT',
      path: `/v1.0/iot-03/devices/${deviceId}`,
      body: update
    })

    res.status(200).json({
      ...result,
      message: 'Device updated successfully!'
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

//
// Get all devices under a user's account by user ID
//
router.get('/', async (req, res) => {
  try {
    const response = await tuya.request({
      method: 'GET',
      path: `/v1.0/users/${uid}/devices`
      // body: {},
    })

    res.status(200).json({
      ...response.result
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

//
// Get device details by id
//
router.get('/:id', async (req, res) => {
  try {
    const deviceId = req.params.id

    const response = tuya.request({
      method: 'GET',
      path: `/v1.0/devices/${deviceId}`
      // body: {},
    })

    await response.then(result => {
      res.status(200).send(result)
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

//
// Get device available data points
//
router.get('/:id/status', async (req, res) => {
  try {
    const deviceId = req.params.id

    const response = tuya.request({
      method: 'GET',
      path: `/v1.0/devices/${deviceId}/status`
      // body: {},
    })

    await response.then(result => {
      res.status(200).json(result)
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

//
// Get device available functions
//
router.get('/:id/functions', async (req, res) => {
  try {
    const deviceId = req.params.id

    const response = tuya.request({
      method: 'GET',
      path: `/v1.0/iot-03/devices/${deviceId}/functions`
      // body: {},
    })

    await response.then(result => {
      res.status(200).send(result)
    })
  } catch (error) {
    console.log('ERROR', error)
  }
})

export { router as deviceRoutes }

After starting the server, if no errors, the API is ready to be consumed. So in the Frontend as usual, by making a GET request to http://localhost:3399/api/v1/devices/:id, where :id is the deviceId connected to your Tuya Cloud project, you should get the device details.

I had no issues with CORS with this set up!

Please leave your enhancement ideas, such as a tuya middleware function or any other type of set up that works for you!

@JimRh
Copy link

JimRh commented Oct 12, 2022

For anyone coming around the issue, the following assumes you've already created an account on Tuya IoT Platform as also a Cloud Project in order to get the required credentials, aka Client ID & Client Secret, and the project API endpoint (Data center url address).

I was facing the same issue regarding CORS policy, but I've solved that by abstrating tuya-connector-nodejs in order to create an ordinary nodejs / express server. Also I've used cors package to configure the Access-Control-Allow-Origin CORS header... I'm sharing the simplified Custom API set up with its own routes which then call tuya-connector-nodejs functions.

On Tuya Iot Platform -> Cloud -> Development: I've left the Cloud Authorization IP Allowlist option disabled, as my intention was to run the server on localhost only.

Nodejs / Express:

.env

TUYA_DATA_CENTER_URL=https://openapi.tuyaus.com
TUYA_CLIENT_ID=
TUYA_CLIENT_SECRET=
TUYA_IMAGE_URL=https://images.tuyacn.com/

src/config.js

import dotenv from 'dotenv';
dotenv.config()

// Tuya Cloud - IoT Platform
const config = {
  baseUrl: `${process.env.TUYA_DATA_CENTER_URL}`, // Western America Data Center
  clientId: `${process.env.TUYA_CLIENT_ID}`, // Access ID / Client ID
  clientSecret: `${process.env.TUYA_CLIENT_SECRET}`, // Access Secret / Client Secret:
  apiImageUrl: `${process.env.TUYA_IMAGE_URL}` // https://images.tuyacn.com/
};

if (!Object.values(config).every(Boolean)) {
  throw new Error(
    'Please create .env from .env.example and specify all values',
  );
}

export default config;

src/index.js

import dotenv from 'dotenv'
import express from 'express'
import axios from 'axios'
import cors from 'cors'

// Custom Tuya Api Routes
import { routes } from './routes/index.js'

const router = express.Router()

dotenv.config()
const port = 3399
const app = express()

app.use(express.urlencoded({ extended: true }))
app.use(express.json({ limit: '32mb' }))

app.use(cors({ origin: '*' })) // This alone didn't work!
app.use(
  cors({
    origin: [
      'https://openapi.tuyeu.com',
      'https://openapi.tuyaus.com',
      'http://localhost:3000',
      'http://localhost:3399',
    ],
    methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'], 
    allowedHeaders: [
      'Origin',
      'X-Requested-With',
      'Content-Type',
      'Accept',
      'Signature-Headers',
      'client_id',
      'access_token',
      'sign',
      'sign_method',
      'stringToSign',
      't',
      'nonce',
    ], 
  })
)

app.use(routes)

app.listen(port, () => {
    console.log(
      `TuyaApi Started ✓ - Express listening on port : ${port} - press Ctrl-C to terminate.`
    )
})

src/routes/index.js

import express from 'express'
const router = express.Router()

// Main Api Routes
import { apiRoutes } from './api/index.js' // src/routes/api/index.js

const api = '/api/v1' 

router.use(api, apiRoutes)
router.use(api, (req, res) => res.status(404).json('No API route found'))

export { router as routes }

src/routes/api/index.js

import express from 'express'
const router = express.Router()

import { deviceRoutes } from './devices.js' // src/routes/api/device.js
import { userRoutes } from './user.js' // src/routes/api/user.js

// Device Routes
router.use('/device', deviceRoutes)

// User Routes
router.use('/users', userRoutes)

// Other Routes...

export { router as apiRoutes }

src/routes/api/device.js

Here tuya-connector-nodejs functions are wrapped within ordinary express router logic...

import express from 'express'
const router = express.Router()
import axios from 'axios'

import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { RedisTokenStore } from '../../utils/tokenStore.ts';
import Redis from 'ioredis';
const redis = new Redis();

import config from '../../config.js';
const { baseUrl, clientId, clientSecret } = config;

const tuya = new TuyaContext({
  baseUrl: baseUrl, //'https://openapi.tuyaus.com',
  accessKey: clientId,
  secretKey: clientSecret,
  store: new RedisTokenStore(redis),
  rpc: axios
});

//
// Get device by id
//

router.get('/:id', async (req, res) => {
  try {
    const deviceId = req.params.id

    const response = tuya.request({
      method: 'GET',
      path: `/v1.0/devices/${deviceId}`,
      body: {},
    })

    await response.then(result => {
      res.status(200).send(result)
    })

  } catch (error) {
    console.log('ERROR', error);
  }
})

// ... rest of routes

// router.get()

// router.post()

...

export { router as deviceRoutes }

After starting the server, if no errors, the API is ready to be consumed. So in the Frontend as usual, by making a GET request to http://localhost:3399/api/v1/devices/:id, where :id is the deviceId connected to your Tuya Cloud project, you should get the device details.

I had no issues with CORS with this set up!

Please leave your enhancement ideas, such as a tuya middleware function or any other type of set up that works for you!

Hi did you run the tuya connector node sdk and this node server together,or you just took the functions from their code and that redis token they have in their github,I was facing sign invalid issue.

@JimRh
Copy link

JimRh commented Oct 13, 2022

@monecchi Can you share your tokenStore.ts file code

@monecchi
Copy link

monecchi commented Nov 3, 2022

Hey @JimRh! You're right, I use their suggested tokenStore solution based on redis. The project I've set up does not use typescript ... I've updated the answer code blocks to include an abstraction of the way I'm connectng to the redis server (remote or local).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants