I am building a web app that uses Ember for the client side and Node for the server side. The database I am using is Mongo. Everything is working on the server side (I am able to use Postman to GET, POST, PUT, and DELETE users. I think I almost have everything hooked up on the client side, but Ember is throwing me one final error when I navigate to the /users route:
ember.debug.js:28535 Error while processing route: users Assertion Failed: You must include an 'id' for user in an object passed to 'push' Error: Assertion Failed: You must include an 'id' for user in an object passed to 'push'
Any ideas why this is happening/ how to fix it?
Here are the relevant parts of my server.js file:
var User = require('./models/user');
// configure app to use bodyParser()
// this will let us get the data from a POST
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Add CORS headers
app.use(function (request, response, next) {
response.header("Access-Control-Allow-Origin", "http://localhost:4200");
response.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
response.header("Access-Control-Allow-Resource", "*");
response.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
next();
});
var port = process.env.PORT || 8080; // set our port
// ROUTES FOR OUR API
var router = express.Router(); // get an instance of the express Router
// test route to make sure everything is working (accessed at GET http://localhost:8080/api)
router.get('/', function(request, response) {
response.json({ message: 'hooray! welcome to our api!' });
});
// more routes for our API will happen here
// REGISTER OUR ROUTES
// all of our routes will be prefixed with /api/v1
app.use('/api/v1/', router);
// START THE SERVER
app.listen(port);
console.log('Magic happens on port ' + port);
// more routes for our API will happen here
// on routes that end in /users
// ----------------------------------------------------
router.route('/users')
// create a user (accessed at POST http://localhost:8080/api/v1/users)
.post(function(request, response) {
var user = new User(); // create a new instance of the User model
user.name = request.body.name; // set the user's name (comes from the request)
user.email = request.body.email; // set the users's email property
user.password = request.body.password; //set the user's password property
// save the user and check for errors
user.save(function(error) {
if (error)
response.send(error);
response.json({ message: 'User created!' });
});
})
// get all the users (accessed at GET http://localhost:8080/api/v1/users)
.get(function (request, response) {
User.find(function (error, users) {
if (error) response.send(error);
response.json(users);
});
});
// on routes that end in /users/:user_id
// ----------------------------------------------------
router.route('/users/:user_id')
// get the user with that id (accessed at GET http://localhost:8080/api/users/:user_id)
.get(function (request, response) {
User.findById(request.params.user_id, function (error, user) {
if (error) response.send(error);
response.json(user);
});
})
// update the user with this id (accessed at PUT http://localhost:8080/api/users/:user_id)
.put(function (request, response) {
// use our user model to find the user we want
User.findById(request.params.user_id, function(error, user) {
if (error) response.send(error);
// update the user info
user.name = request.body.name;
user.email = request.body.email;
user.password = request.body.password;
// save the user
user.save(function(error) {
if (error) response.send(error);
response.json({ message: 'User updated!' });
});
});
})
// delete the user with this id (accessed at DELETE http://localhost:8080/api/users/:user_id)
.delete(function (request, response) {
User.remove({
_id: request.params.user_id
}, function(error, user) {
if (error) res.send(err);
response.json({ message: 'Successfully deleted' });
});
});
Here is my application adapter (on the Ember side):
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
host: 'http://localhost:8080',
namespace: 'api/v1'
});
Here is my serializer (on the Ember side):
import JSONAPISerializer from 'ember-data/serializers/json-api';
import DS from 'ember-data';
export default JSONAPISerializer.extend({
primaryKey: '_id'
});
export default DS.JSONSerializer;
Here is my model (on the Ember side):
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
name: attr('string'),
email: attr('string'),
password: attr('string')
});
Here is my users.js route (on the Ember side):
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.findAll('user');
},
actions: {
createUser(newName, newEmail, newPassword) {
this.store.createRecord('user', {
name: newName,
email: newEmail,
password: newPassword
}).save();
},
updateUser(user) {
user.save();
},
deleteUser(user) {
user.destroyRecord();
}
}
});
From your posted code your Application adapter is defined as RESTAdapter and your serializer is defined as JSONAPISerializer. Ember will expect JSON API format as defined on jsonapi ORG site
To solve you can do two things. Make node output JSON api format with data id and attributes format using this library for example: https://github.com/SeyZ/jsonapi-serializer or just make sure that you use REST serializer and adapter for your resources.
Try to define your application serializer like this
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
primaryKey: '_id',
serializeId: function(id) {
return id.toString();
}
});
Make sure that JSON output include something like this:
{
users: [
{"_id": "13ac3f0d4e29d794d9f62898","name":"", "email": "" },
{"_id": "13ac3f0d4e29d794d9f62899","name":"", "email": "" }
]
}
Another small issue is that your API endpoint on server side that creates, deletes or updates the user returns message instead of the users resource
user.save(function(error) {
if (error) response.send(error);
response.json({ message: 'User updated!' });
});
You should always return the updated resource, in this case you need to return the user as you do in GET /users/ID route with proper status and error code, for example:
When creating a user ember would expect that you return 201 CREATED status code and json response of user resource. For DELETE and PATCH/PUT (Update) you need to return status code 200. In all cases you need to use response.json(user) instead of outputting messages.
If you are not able to process the request (for example when creating a user email is already taken) you should respond with 422 and return proper error message. See this for handling errors: http://emberjs.com/api/data/classes/DS.Errors.html
Hope it helps.
Related
I created an express middleware that checks the user token as authorization before proceeding:
import jwt from 'jsonwebtoken';
export const verifyAuthorization = async (req, res, next) => {
try {
const token = req.headers.authorization.split(' ')[1];
if (!token) return res.status(403).send({ error: 'Not authorized' });
const tokenData = jwt.verify(token, 'secret_code');
req.userData = {
userId: tokenData.userId,
fullName: tokenData.fullName,
section: tokenData.section,
groupId: tokenData.groupId,
level: tokenData.level,
};
next();
} catch (error) {
console.log(error);
res.status(403).send({ error: 'Not authorized' });
}
};
I applied this middleware for all endpoints of a route like this:
import { Router } from 'express';
const router = Router();
import item from '../controllers/item_controllers.js';
import { verifyAuthorization } from '../middleware/verifyAuthorization.js';
router.use(verifyAuthorization);
router.post('/all', item.all);
router.post('/new', item.newOne);
router.post('/edit', item.edit);
export default router;
The problem is that if there's a problem with the authorization, it sends back the status code but not the body. In my React app I see the 403 error on console but it logs the response as "undefined".
The controller functions send back the json body perfectly fine. I don't understand what's wrong with the middleware here.
Any ideas??
Thank you
I fixed it. There was a problem with the custom hook I created for http requests. It was finishing the requests on error without sending back any data. That's why I saw the status code error but not the body. I logged the response on the hook and it gets there. I just deleted the "return" keyword on the error block in the hook and I got the response's body perfectly.
Thank you everybody for your time.
So I'm trying to programmatically delete Wasabi CDN objects from one of my buckets. My request is sending back 204 and showing success but nothing is being moved/deleted. I'm using node/javascript to do this.
Here is my function that is supposed to delete the bucket.
import expressAsyncHandler from 'express-async-handler'
import User from '../../models/User.js'
import axios from 'axios'
import aws4 from 'aws4'
/**
* #desc: THIS is going to be a testing function that will be added into admin delete user and all related docs.
* #route: DELETE
* #access: Private - Admin Route for when deleting user will delete the CDN username bucket aswell
* #goalHere: The goal of this function is to delete the user bucket from CDN. So that if we d
* #comment: This is a testing function that will be added into deleteVideo.js. Unless we just await this function in deleteVideo.js.
*/
export const deleteUserBucket = expressAsyncHandler(async (req, res, next) => {
try {
const user = await User.findOne({ username: req.user.username })
const username = user.username
let request = {
host: process.env.CDN_HOST,
method: 'DELETE',
url: `https://s3.wasabisys.com/truthcasting/${username}?force_delete=true`,
path: `/truthcasting/${username}?force_delete=true`,
headers: {
'Content-Type': 'application/json',
},
service: 's3',
region: 'us-east-1',
maxContentLength: Infinity,
maxBodyLength: Infinity,
}
let signedRequest = aws4.sign(request, {
accessKeyId: process.env.CDN_KEY,
secretAccessKey: process.env.CDN_SECRET,
})
//delete the Host and Content-Length headers
delete signedRequest.headers.Host
delete signedRequest.headers['Content-Length']
const response = await axios(signedRequest)
console.log(response.data)
console.log('successfully deleted user bucket', response.status)
return res.status(200).json({
message: `Successfully deleted user bucket`,
})
} catch (error) {
console.log(error)
return res.status(500).json({
message: `Problem with deleting user bucket`,
})
}
})
export default deleteUserBucket
When I send the http DELETE request in POSTMAN to {{dev}}api/admin/deleteuserbucket it then gives me a response of 204 ok and this is the response.
{
"message": "Successfully deleted user bucket"
}
I then go to my Wasabi CDN Buckets to check if it is deleted, in this case it's goodstock and it's still there. Feel like I'm missing something dumb here.
In S3, the delete bucket API call return 204 No content and an empty response body on successful delete.
With that URL, you are making a delete request on an object and not the bucket:
URL: `https://s3.wasabisys.com/truthcasting/${username}?force_delete=true`
The username passed in this URL will be interpreted as a key and S3 will look for an object in the root of the bucket.
Also why not using the AWS SDK to delete the bucket instead of reimplementing all of this. Check the AWS docs for this.
So for deleting contents that're inside of your root bucket, you need to point it to the complete object. That being said the way I had it set up in the original post code was returning 204("which is expected from Wasabi API") and not deleting anything due to the fact that I wasn't pointing it to the complete object path. Also I've found out that if you want to do batch delete instead of deleting one file, one by one you can use the aws-sdk node package to do a get request to your object, then use that response to loop through the object and remove what you need.. Here is an example. Hopefully this can help someone in the near future.
import expressAsyncHandler from 'express-async-handler'
import User from '../../models/User.js'
import axios from 'axios'
import aws4 from 'aws4'
import errorHandler from '../../middleware/error.js'
import AWS from 'aws-sdk'
/**
* #desc: THIS is going to be a testing function that will be added into admin delete user and all related docs.
* #route: DELETE
* #access: Private - Admin Route for when deleting user will delete the CDN username bucket aswell
* #goalHere: The goal of this function is to delete the user bucket from CDN. So that if we d
* #comment: This is a testing function that will be added into deleteVideo.js. Unless we just await this function in deleteVideo.js.
*/
export const deleteUserBucket = expressAsyncHandler(async (req, res, next) => {
const username = req.body.username
try {
// Connection
// This is how you can use the .aws credentials file to fetch the credentials
const credentials = new AWS.SharedIniFileCredentials({
profile: 'wasabi',
})
AWS.config.credentials = credentials
// This is a configuration to directly use a profile from aws credentials file.
AWS.config.credentials.accessKeyId = process.env.CDN_KEY
AWS.config.credentials.secretAccessKey = process.env.CDN_SECRET
// Set the AWS region. us-east-1 is default for IAM calls.
AWS.config.region = 'us-east-1'
// Set an endpoint.
const ep = new AWS.Endpoint('s3.wasabisys.com')
// Create an S3 client
const s3 = new AWS.S3({ endpoint: ep })
// The following example retrieves an object for an S3 bucket.
// set the details for the bucket and key
const object_get_params = {
Bucket: 'truthcasting',
Prefix: `${username}/`,
//Key: `cnfishead/videos/4:45:14-PM-5-6-2022-VIDDYOZE-Logo-Drop.mp4`,
// Key: `cnfishead/images/headshot.04f99695-photo.jpg`,
}
// get the object that we just uploaded.
// get the uploaded test_file
// s3.getObject(object_get_params, function (err, data) {
// if (err) console.log(err, err.stack) // an error occurred
// else console.log(data) // successful response
// })
// get the object that we just uploaded.
// get the uploaded test_file
await s3.listObjectsV2(object_get_params, (err, data) => {
if (err) {
console.log(err)
return res.status(500).json({
message: 'Error getting object',
error: err,
})
} else {
console.log(data)
//TODO Change this for loop to a async for loop. Like this: for await (const file of data.Contents) { }
for (let i = 0; i < data.Contents.length; i++) {
const object_delete_params = {
Bucket: 'truthcasting',
Key: data.Contents[i].Key,
}
s3.deleteObject(object_delete_params, (err, data) => {
if (err) {
console.log(err)
return res.status(500).json({
message: 'Error deleting object',
error: err,
})
} else {
console.log(data)
}
})
}
if (data.IsTruncated) {
console.log('Truncated')
getObjectFromBucket(req, res, next)
}
//console.log('Not Truncated')
res.status(200).json({
message: `Successfully retrieved + deleted ${data.Contents.length} objects`,
data: data,
})
}
})
} catch (error) {
console.log(error)
errorHandler(error, req, res)
}
})
export default deleteUserBucket
I am trying to debug a failing JWT auth setup, which always returns a 401.
My passport setup (middleware/auth.js)
import passport from 'passport'
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
module.exports = function() {
var options = {};
options.jwtFromRequest = ExtractJwt.fromAuthHeader()
options.secretOrKey = 'superdupersecret'
var strategy = new JwtStrategy(options, function(payload, done) {
console.log('this is not printing') <---------------
var user = payload.sub || null;
if (user) {
return done(null, { id: user._id });
} else {
return done(new Error("User not found"), null);
}
});
passport.use(strategy);
return {
initialize: () => {
console.log('this only prints on boot'); <---------------
return passport.initialize();
},
authenticate: () => {
console.log('this too') <---------------
return passport.authenticate("jwt", {session: false});
}
};
};
My server.js file where I initialize passport:
import express from 'express'
(...)
var auth = require("./middleware/auth.js")();
// Instantiate app
const app = express();
// Initialize passport for auth use
app.use(auth.initialize())
And my protected route that always returns a 401:
import express from 'express'
var auth = require("../middleware/auth.js")();
const userRouter = express.Router()
userRouter.get('/dashboard', auth.authenticate(), (req, res) => {
res.send('It worked! User id is: ' + req.user + '.')
})
export default userRouter
I have tried to add print statements within the actual passport.js module itself, as well as passport-jwt, with no success.
After the authentication middleware on the protected route, nothing logs.
I have tried a ton of setup permutations over the past 3 days now. Any help would be greatly appreciated
Ok, I followed the tutorial you mentioned and it seems to work.
Here are some notes (some may be obvious, no offense).
Copy exactly the code as the tutorial
After you have everything, you need to "login". Make a POST request to /token. Content type has to be application/json and on the body of the request you need to sent an object with email and password (from tutorial).
After you login, the server returns a token.
Take that token and now make a GET request to /user. In the headers of the request add: Authorization: JWT [your token here]. You have to write "JWT" and the token separated by one space.
The server returns a status 200. I modified so it returns the user.
app.get("/user", auth.authenticate(), function(req, res) {
res.json({user: req.user});
});
I have the following setup right now
test.js
var user = {
username: 'test_user',
email: 'test#test.me',
password: 'you shall not pass',
address: 'No where street'
};
chai.request(app)
.post('/api/v1/users')
.send(user);
I'm handling the post request in my routes/user.js
router.post('/', function(req, res, next) {
console.log('body: ' + req.body);
queries.insertUser(req.body)
.then(function(id) {
return queries.getSingleUser(id);
})
.then(function(user) {
res.status(200).json(user);
})
.catch(function(err) {
next(err);
});
});
req.body ends up being undefined. Any clue as to what might be going wrong?
The code is live at https://ide.c9.io/burtonium/node-from-scratch if anybody wants to have a look.
req.body being undefined is generally caused by either not using the body-parser middleware in Express, or declaring it incorrectly (for instance, after the route that wants to access req.body).
Assuming that Chai sends JSON, add this to your Express app:
app.use(require('body-parser').json());
(before you declare the routers)
We are testing our loopback API code using spec.js files like this:
Require libs:
var app = rewire('../..');
var request = require('supertest');
var assert = require('chai').assert;
json helper method to standardize headers and content type:
function json(verb, url) {
return request(app)[verb](url)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect('Content-Type', /json/);
}
A test of a custom remote method that requires auth:
describe("Order remote methods", function() {
var accessTokenId, userId;
// authenticate before each test and save token
before(function(done) {
json('post', '/api/People/login')
.send({ email: 'user#email.com', password: 'password' })
.expect(200)
.end(function(err, res) {
accessTokenId = res.body.id;
userId = res.body.userId;
assert(res.body.id);
assert(res.body.userId);
done();
});
});
it("should fetch user orders", function(done) {
json('get', '/api/Orders/specialOrders')
.set('Authorization', accessTokenId)
.send({id: userId})
.expect(200)
.end(function(err, res) {
var orders = res.body.orders;
assert(Array.isArray(orders), "Orders should be an array");
// more asserts for explicit data values
done();
});
});
});
/api/Orders/specialOrders is a custom remote method that does a custom query on the Order model, which works as expected. But when I add a beforeRemote hook for this model, it does not get triggered by running the test. Is this expected or is my test setup not complete?
Remote hook:
Order.beforeRemote('specialOrders', function(ctx, unused, next) {
console.log('[userOrders]');
console.log('ctx req token: ', ctx.req.accessToken.userId);
console.log('ctx args: ', ctx.req.params.id);
// prevent remote method from being called
// even without a next(), remote is executed!
next(new Error('testing error'));
});
Running the same custom method via the Explorer UI, the beforeRemote hook is triggered as expected, and reports the custom error (or hangs when the next() is not present).
Is it possible to get supertest to trigger remote hooks in tests like this or am I missing some app setup in the spec file?