Set strapi permission to specific collection data - strapi

I am planning in developing a large structure for a client and I am looking into using Strapi to manage the content and the APIs.
Before even digging deeper I would like to ask if anyone know if there is an existing plugin to set limitations to the collections data.
For example, I create a collection called restaurant where I am going to have 1 field: name. Then I create 2 restaurants named "The Optimist" & "The Negative"
After, I create 2 more user for my back end: Maria & Julia.
Is there any existing way to set Maria to only be able to edit "The optimist" & Julia to only edit "The Negative"?

Explaination
Well there is a way to limit users from performing specific actions on the entire collection directly out of the box, but limiting to specific entries needs customization in controller.
I would recommend you to go through Users, Roles & Permissions guide from the Strapiv4 documentation for better understanding.
Attaching a snapshot below you give you a brief idea. As you can see, generic actions like create, update, delete etc. can be permitted only to specific roles, which in turn can be assigned to the users of your choice.
# Image showing permissions being assigned to a role
# Image showing role being assigned to a user
Solution
Coming to your question on limiting users to specific entries, you can easily achieve this by writing custom code that checks for the entry id and the role that's trying to update the restaurant. Check the snippet below:
// src/api/resto/controllers/resto.js
"use strict";
/**
* resto controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::resto.resto", ({ strapi }) => ({
async update(ctx) {
const { id } = ctx.params;
const { role } = ctx.state.user;
// if you don't want to hard code the ids, you can do a findOne for the id and do a check on the resto name.
// Assuming id 4 corresponds to entry "The optimist"
// Assuming id 5 corresponds to entry "The Negative"
if ((id === 4 && role.name !== "Role Maria") || (id === 5 && role.name !== "Role Julia")) {
return ctx.badRequest("You are not allowed to update this entry", {
id: id,
role: role.name,
});
}
const entity = await strapi
.service("api::resto.resto")
.update(id, { data: ctx.request.body });
const response = await super.findOne(ctx);
// const sanitizedEntity = await this.sanitizeOutput(response, ctx);
// return this.transformResponse(sanitizedEntity);
return response;
},
}));

Related

How to create Strapi component data in a lifecycle

I want to add content to a repeatable component in my beforeUpdate hook. (adding a changed slug to a “previous slugs” list)
in v3, I could just push new data on the component array and it would save.
in v4, it doesn’t work like that. Component data now holds __pivot: and such. I do not know how to add new data to this. I’ve tried adding a component with the entityService first, and adding that result to the array. It seemed to work, but it has strange behavior that the next saves puts in two entries. I feel like there should be an easier way to go about this.
It seems like the way to go about this is to create the pivot manually:
// create an entry for the component
const newRedirect = await strapi.entityService.create('redirects.redirect', {
data: {
from: oldData.slug,
},
});
// add the component to this model entry
data.redirects = [...data.redirects, {
id: newRedirect.id,
__pivot: { field: 'redirects', component_type: 'redirects.redirect' },
}];
But this feels pretty hacky. If I change the components name or the field key, this will break. I'd rather have a Strapi core way of doing this
the way strapi currently handles components is by providing full components array, so in case you want to inject something, you have to read components first and then apply full update, if it makes it clear.
Update
So after few hours of searching, had to do few hours of trail and error, however here is the solution, using knex:
module.exports = {
async beforeUpdate(event) {
// get previous slug
const { slug: previousSlug } = await strapi.db
.query("api::test.test")
.findOne({ where: event.params.where });
// create component
const [component] = await strapi.db
// this name of components table in database
.connection("components_components_previous_slugs")
.insert({ slug: previousSlug })
.returning("id");
// append component to event
event.params.data.previousSlugs = [
...event.params.data.previousSlugs,
{
id: component.id,
// the pivot, you have to copy manually
// 'field' is the name of the components property
// 'component_type' is internal name of component
__pivot: {
field: "previousSlugs",
component_type: "components.previous-slugs",
},
},
];
},
};
So, seems there is no service, or something exposed in strapi to create component for you.
The stuff that also required to be noted, on my first attempt i try to create relation manually in tests_components table, made for me after i added a repeatable component, to content-type, but after an hour more i found out that is WRONG and should not be done, seems strapi does that under the hood and modifying that table actually breaks logic...
so if there is more explanation needed, ping me here...
result:
You can update, create and delete component data that is attached to a record with Query Engine API, which is provided by Strapi.
To modify component data you just need the ID.
const { data } = event.params;
const newData = {
field1: value1,
etc...
};
await strapi.query('componentGroup.component').update({
where: { id: data.myField.id },
data: newData
})
When you have a component field that equals null you need to create that component and point to it.
const tempdata = await strapi.query('componentGroup.component').create(
{ data: newData }
);
data.myField = {
id: tempdata.id,
__pivot: {
field: 'myField',
component_type: 'componentGroup.component'
}
}
Se the Strapi forum for more information.

hosted parse Cloud code find(...) does not retrieve sessionToken

I am migrating Parse.com cloud code to hosted parse-server. Mongodb is already migrated.
The code is supposed to retrieve a user's sessionToken according to its google plus id. The find operation succeeds and a user is returned but the user does not have any sessionToken.
EDIT regarding mongodb:
Checking mongo db directly - the _User table record holds a _session_token value. In the _Session table I could not find that _session_token. Also I could not find any entry for that _User (I concatenated _User$ when searching).
The code is (more or less) so:
Parse.Cloud.define("getParseUserSessionToken", function(request, response) {
var googlePlusId = ...;
// here we're doing some code to get the google id
// assume it is set
var googleIdQuery = new Parse.Query(Parse.User);
googleIdQuery.equalTo(GOOGLE_PLUS_ID_FIELD_KEY, googlePlusId);
searchUserQuery.find({
useMasterKey: true
}).
then(function(users) {
if (users && users.length > 0) {
var user = users[0]; //user exist
var token = user.getSessionToken();
if (token) {
//user exist, return sessionToken
// ....
} else {
// CODE REACHES HERE AS THERE'S NO TOKEN
// return error: "no token"
}
}
}
As you can see I pass userMasterKey: true to find()
With a debugger I can see that I get a user with data, but without a token. I tried several things but nothing works.
Any idea what am I missing?

Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

How do you update objects in a Parse.com database using Xamarin?

I have an online Parse.com database and I can create objects and query them but not update. In the Xamarin section of the Parse.com documentation it only tells you how to update an object directly after you've created it which I don't want to do. I tried adapting what the documentation says for other platforms but it hasn't worked, I have also tried querying the database and entering the new field values directly after that but it treats them as separate functions. Does anyone have any help?
Parse.com documentation:
// Create the object.
var gameScore = new ParseObject("GameScore")
{
{ "score", 1337 },
{ "playerName", "Sean Plott" },
{ "cheatMode", false },
{ "skills", new List<string> { "pwnage", "flying" } },
};
await gameScore.SaveAsync();
// Now let's update it with some new data. In this case, only cheatMode
// and score will get sent to the cloud. playerName hasn't changed.
gameScore["cheatMode"] = true;
gameScore["score"] = 1338;
await gameScore.SaveAsync();
What I tried most recently:
ParseQuery<ParseObject> query = ParseObject.GetQuery("cust_tbl");
IEnumerable<ParseObject> customers = await query.FindAsync();
customers["user"] = admin;
record["score"] = 1338;
await record;
In your example, you are getting an list (IEnumerable) of objects instead of single object. Instead, try something like this:
ParseQuery<ParseObject> query = ParseObject.GetQuery("cust_tbl");
// you need to keep track of the ObjectID assigned when you first saved,
// otherwise you will have to query by some unique field like email or username
ParseObject customer = await query.GetAsync(objectId);
customer["user"] = admin;
customer["score"] = 1338;
await customer.SaveAsync();

Find objects not in a relation

I have a custom object Team in Parse with a relation field for the default User object. What I would like to do is retrieve all User objects which are not related to any Team object. Can anyone point me in the right direction on how to do this using the JavaScript SDK? I've been going over the documentation for the Query object but I can't find anything.
Perhaps another type of relation, or placing the relation at another place is a better solution. What I want to accomplish is the following: Each user is allowed to be in one team and one team only. In addition I need to be able to query the following information from Parse:
I want to retrieve the User objects of all the users assigned to a team
I want to retrieve the User objects of all the users who are not assigned to any team
I have tried using a join table with both the user and team object ids. Then I tried to following query to get all users not assigned to a team:
var teammember = Parse.Object.extend('TeamMember'),
query = new Parse.Query("User");
var innerQuery = new Parse.Query("TeamMember");
query.doesNotMatchQuery('user', innerQuery);
query.find({
success: function(results) {
response.success(results);
},
error : function(error) {
response.error(error);
}
})
But this just gets me the following response: error: "{"code":102,"message":"bad type for $notInQuery"}".
I like the Relation type as I can add or remove multiple members at once with a single call to the REST API. I also have no problems retrieving the information on team members when using the Relation type to connect the users to the teams. It is just getting the users which are not assigned to any team that is giving me problems.
It doesn't sound like you need a relation at all. Instead, add a Pointer column to User that points to Team. It ensures that a User can only belong to one team, and your other requirements can be captured as follows.
// All users assigned to a team
query = new Parse.Query('User');
query.exists('team');
// All users assigned to a specific team
query = new Parse.Query('User');
query.equalTo('team', specificTeam);
// All unassigned users
query = new Parse.Query('User');
query.doesNotExist('team');
Update: If you need to support multiple teams per User in the future, then I would suggest creating a Parse table called Membership with two columns: a Pointer to User and a Pointer to Team. This essentially gives you more control than relying on Parse relations, but it gets a little more complicated.
_ = require('underscore'); // Or lodash
// All users assigned to a team
query = new Parse.Query('Membership');
query.find().then(function (results) {
// http://underscorejs.org/#uniq
users = _.uniq(results, false, function (user) { return user.id; });
});
// All users assigned to a specific team
query = new Parse.Query('Membership');
query.equalTo('team', specificTeam);
// All unassigned users
var assignedUsers = []
var unassignedUsers = []
memberQuery = new Parse.Query('Membership');
userQuery = new Parse.Query('User');
memberQuery.find().then(function (memberResults) {
// http://underscorejs.org/#map
var ids = _.map(memberResults, function (user) { return user.id; });
// http://underscore.js.org/#uniq
assignedUsers = _.uniq(ids);
userQuery.find();
}).then(function (userResults) {
var users = _.map(userResults, function (user) { return user.id; });
// http://underscorejs.org/#difference
unassignedUsers = _.difference(users, assignedUsers);
});
To add and remove Users to/from Teams, you would create Membership objects and save API calls with Parse.Object.saveAll() and Parse.Object.destroyAll().
I ran into trouble with the answer provided by Seth. When retrieving the users not assigned to a team the difference between the two arrays would be incorrect. I am assuming this is due to the assignedUsers having object of type Membership and userResults being of type User. This would make it impossible for underscore to make a proper match.
I would up using this as my Cloud Code:
Parse.Cloud.define("getTeamlessUsers", function(request, response) {
var _ = require("underscore"),
assignedUsers = [],
companyUsers = [],
memberQuery = new Parse.Query("TeamMembers"),
userQuery = new Parse.Query("User"),
index,
ubound;
memberQuery.find().then(function(memberResults) {
// Make sure each User ID will appear just once
memberResults = _.unique(memberResults, false, function(item) { return item.get('user').id; });
// Loop over the unique team members and push the User ID into the array
for (index = 0, ubound = memberResults.length; index < ubound; index++) {
var user = memberResults[index].get("user");
assignedUsers.push(user.id);
}
// Get al the users
return userQuery.find();
}).then(function(userResults) {
// Loop over all the users and push the ID into the array
for (index = 0, ubound = userResults.length; index < ubound; index++) {
companyUsers.push(userResults[index].id);
}
// Create an array of user IDs which are not present in the assignedUsers array
var result = _.difference(companyUsers, assignedUsers);
// Return the IDs of user not assigned to any team
response.success(result);
}).fail(function(error) {
response.error(error);
});
});

Resources