How to setup dynamic URLs for Resources in admin-on-rest? - admin-on-rest

I've a requirement where the below resources are accessed based on the type of user logged in.
R1: /mit/oss/12345/peers
R2: /mit/md/6879/ngrp
R1 should be accessible by an user with id: 12345. And R2 should be accessible by an user with id - 6879.
The question is - how to define Resource URLs with dynamic values (like: userId in the URLs) based on the user who is logged it. I am aware of aor-permissions library to switch the Menus based on user permission, but is it possible to define the resources themselves dynamically with ids in the URLs?

You can write a wrapper on your rest client that can intercept the call and dynamically generate the URL.
Basically decorate the rest client like in the docs here --> https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
You can then check for a case like in below psuedocode
if (type === 'AOR_REST_TYPE' && resource === 'BASE_RESOURCE') {
if (getUserFromLocalStorage === usr1) {
url = url1
} else {
url = url2
}
options.method = 'GET';
// other options
}

Here is a simple example of remapping the resource URL using a map.
import {simpleRestClient} from 'admin-on-rest';
// Update the remap table appropriately to map from a resource name to a different path
const remap = {
"resource1" : "resource1Remapped",
"releasepresets" : "productionintents/releasepresets"
}
const simpleRestClientWithResourceUrlRemap = (apiUrl) => {
var client = simpleRestClient(apiUrl);
return (type, resource, params) => {
if (remap[resource]) {
console.log('remapping resource from ' + resource + ' to ' + remap[resource]);
resource = remap[resource];
}
return client(type, resource, params);
}
}
export default (simpleRestClientWithResourceUrlRemap);
Instead of a simple remap, a function with logic could be used.

Related

Set strapi permission to specific collection data

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;
},
}));

Implementing file attachments in JHipster

I have a monolithic JHipster (v5.6.1) project, and I need to implement file attachments to an entity.
I don't know where to start with this. DTOs are sent to REST controllers to create or update them, how should I send the file?
I could attach the Base64 to my DTO and send it that way, but I'm not sure how (are there any angular plugins to accomplish this?). This would require extra work for the server and, I think, extra file size.
Another option is to send the entity (DTO) and file separately, again I'm not entirely sure how.
I see no reason to leave this question unanswered since I have implemented attachments successfully in several of my projects now.
If you prefer, you can skip this explanation and check the github repository I created at vicpermir/jhipster-ng-attachments. It's a working project ready to fire up and play.
The general idea is to have an Attachment entity with all the required fields (file size, name, content type, etc...) and set a many-to-many relation to any entity you want to implement file attachments for.
The JDL is something like this:
// Test entity, just to showcase attachments, this should
// be the entity you want to add attachments to
entity Report {
name String required
}
entity Attachment {
filename String required // Generated unique filename on the server
originalFilename String required // Original filename on the users computer
extension String required
sizeInBytes Integer required
sha256 String required // Can be useful for duplication and integrity checks
contentType String required
uploadDate Instant required
}
// ManyToMany instead of OneToMany because it will be easier and cleaner
// to integrate attachments into other entities in case we need to do it
relationship ManyToMany {
Report{attachments} to Attachment{reports}
}
I have both filename and originalFilename because one of my requirements was to keep whatever file name the user uploaded it with. The generated unique name that I use on the server side is transparent to the user.
Once you generate a project with a JDL like that, you will have to add the file payload to your DTO (or entity if you don't use DTOs) so that the server can receive it in base64 and store it.
I have this in my AttachmentDTO:
...
private Instant uploadDate;
// File payload (transient)
private byte[] file;
public Long getId() {
return id;
}
...
Then, you only have to process those byte arrays on the server side, store them and save a reference to the location into the database.
AttachmentService.java
/**
* Process file attachments
*/
public Set<AttachmentDTO> processAttachments(Set<AttachmentDTO> attachments) {
Set<AttachmentDTO> result = new HashSet<>();
if (attachments != null && attachments.size() > 0) {
for (AttachmentDTO a : attachments) {
if (a.getId() == null) {
Optional<AttachmentDTO> existingAttachment = this.findBySha256(a.getSha256());
if(existingAttachment.isPresent()) {
a.setId(existingAttachment.get().getId());
} else {
String fileExtension = FilenameUtils.getExtension(a.getOriginalFilename());
String fileName = UUID.randomUUID() + "." + fileExtension;
if (StringUtils.isBlank(a.getContentType())) {
a.setContentType("application/octet-stream");
}
Boolean saved = this.createBase64File(fileName, a.getFile());
if (saved) {
a.setFilename(fileName);
}
}
}
result.add(a);
}
}
return result;
}
What I do here is check if the attachment already exists (using the SHA256 hash). If it does I use that one, otherwise I store the new file and persist the new attachment data.
What's left now is to manage attachments on the client side. I created two components for this so it is extremely easy to add attachments to new entities.
attachment-download.component.ts
...
#Component({
selector: 'jhi-attachment-download',
template: , // Removed to reduce verbosity
providers: [JhiDataUtils]
})
export class JhiAttachmentDownloadComponent {
#Input()
attachments: IAttachment[] = [];
}
This just calls a mapping that takes the attachment ID, looks for the associated file on the server and returns that file for the browser to download. Use this component in your entity detail view with:
<jhi-attachment-download [attachments]="[your_entity].attachments"></jhi-attachment-download>
attachment-upload.component.ts
...
#Component({
selector: 'jhi-attachment-upload',
template: , // Removed to reduce verbosity
providers: [JhiDataUtils]
})
export class JhiAttachmentUploadComponent {
#Input()
attachments: IAttachment[] = [];
loadingFiles: number;
constructor(private dataUtils: JhiDataUtils) {
this.loadingFiles = 0;
}
addAttachment(e: any): void {
this.loadingFiles = 0;
if (e && e.target.files) {
this.loadingFiles = e.target.files.length;
for (let i = 0; i < this.loadingFiles; i++) {
const file = e.target.files[i];
const fileName = file.name;
const attachment: IAttachment = {
originalFilename: fileName,
contentType: file.type,
sizeInBytes: file.size,
extension: this.getExtension(fileName),
processing: true
};
this.attachments.push(attachment);
this.dataUtils.toBase64(file, (base64Data: any) => {
attachment.file = base64Data;
attachment.sha256 = hash
.sha256()
.update(base64Data)
.digest('hex');
attachment.processing = false;
this.loadingFiles--;
});
}
}
e.target.value = '';
}
getExtension(fileName: string): string {
return fileName.substring(fileName.lastIndexOf('.'));
}
}
Use this component in your entity update view with:
<jhi-attachment-upload [attachments]="editForm.get('attachments')!.value"></jhi-attachment-upload>
Once sent to the server, the files will be stored on the folder you configured in your application-*.yml separated in subdirectories by year and month. This is to avoid storing too many files on the same folder, which can be a big headache.
I'm sure many things could be done better, but this has worked for me.

Path parameters in Angular2

How are we meant to handle path parameters in Angular 2 when doing RESTful calls to a Web Service?
I've found the URLSearchParams object for query parameters but from what I have found it seems we'll have to make do with string-concatenation for the path itself. Like
let url = 'api/v1/something/' + encodeURIComponent(someParameter) +
'/etc/' + encodeURIComponent(anotherParam) + '/and/so/on';
Is there, included in angular2, something that does similar to:
let url = new URL('api/v1/something/{id}/etc/{name}/and/so/on', param1, param2);
I can of course create something similar myself but prefer if there is something included in angular2.
Indeed, you can use string interpolation and nearly exactly the same thing you suggest, assuming id and name are already set and encoded.
let url = new URL(`api/v1/something/${id}/etc/${name}/and/so/on`);
I'll note that currently the ES6 URL class only supports searchParams, but has no notion of path parameters.
Also, I know of no method that will automatically encode your parameters without implementing your own QueryEncoder with URLSearchParams
If anybody is interested, this is what I ended up doing:
export interface PathParameters {
[parameterName: string]: string;
}
export class Url {
public static create(url: string, parameters: PathParameters) {
const placeholders = url.match(/({[a-zA-Z]*})/g);
placeholders.forEach((placeholder: string) => {
const key = placeholder.substr(1, placeholder.length - 2);
const value = parameters[key];
if (!value) {
throw new Error(`parameter ${key} was not provided`);
}
url = url.replace(placeholder, encodeURIComponent(value));
});
return url;
}
}
How to use:
const url = Url.create('/api/cars/{carId}/parts/{partId}', {
carId: carId,
partId: partId
});

Multiple page objects in one test case

So far the example is only using one page object in a test case. Can we have multiple page objects in a test case?
Imagine that I have a test case which required to login, and then followed by creating an user.
So I have two page objects, one for login page and another for user page. We will use the page objects like this?
module.exports = {
'login': function (browser) {
var login = browser.page.login();
login.navigate()
.click('#submit');
},
'create user': function (browser) {
var users = browser.page.users();
users.navigate()
.click('#submit')
.end();
}
}
My code would be like :
module.exports = {
'create user': function (browser) {
const pages = browser.page,
login = pages.login(),
userPage = pages.users();
login.navigate()
.setValue('#username','myuser')
.setValue('#pass','mypass')
.click('#submit',function(){
users.navigate()
.click('#submit')
.end();
})
}
}

How can I see which Pages a User requests in Grails?

I use the Grails Application Info Plugin to get active sessions as:
ScopesInfoService scopesInfoService
List<Map<String, Object>> activeSessionsMap = scopesInfoService.getSessionsInfo()
activeSessionsMap.each { sessionMap ->
def tmpSession = sessionMap.session
}
How can I see which page a user with a session has been requested and is requesting?
Create a filter and store the current controller and action or request.forwardURI for each request in a variable called currentLocation or something. When you access sessions just read that. you can be creative with it and store any data. but not sure this is a proper approach anyway.
storeLocation(controller: '*', action: '*') {
before = {
session.currentLocation = "$controllerName/$actionName"
}
}
This plugin will not give you that. You need to develop something yourself.
You can create a grails Filter that saves to your database (create a new table for this) the information about the session, user and about the URI (your pages) is being requested.
Sample filter:
class UserInfoFilters {
def filters = {
all(controller:'*', action:'*') {
before = {
SessionUserInfoDomainClass s = new SessionUserInfoDomainClass()
// populate your domain class above with the info you need. Examples:
s.user = session.user
s.controller = controllerName
s.save()
}
}
}
}
Then you can easily have some UI (even with scaffolg) of this SessionUserInfoDomainClass to read the info you want.

Resources