how to create custom endpoint in strapi v4 - strapi

I've migrated from strapi v3 to strapi v4 and i wants to create custom endpoint 'events/me' by this code in "src/api/event/controllers" directory:
"use strict";
const { sanitizeEntity } = require("strapi-utils");
module.exports = {
// Get logged in users
async me(ctx) {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
const data = await strapi.services.events.find({ user: user.id });
if (!data) {
return ctx.notFound();
}
return sanitizeEntity(data, { model: strapi.models.events });
},
};
but i encountered to this error:
Cannot find module 'strapi-utils'
it seems that "strapi-utils" there isn't in strapi v4.
by addition, there isn't any "cofing/routes.json" file. this has been replaced by "route/event.js" file.
strapi document couldn't help me. Any help would be appreciated

You could import it from #strapi/utils.
The sanitize function of Strapi has a bit changed in v4: you have to use the Content API and provide the schema for the sanitized object.
Here you can find an example of use: https://github.com/strapi/strapi/blob/main/packages/plugins/users-permissions/server/controllers/user.js

Related

Strapi returns 404 for custom route only when deployed to Heroku

I have created a custom route in Strapi v4 called "user-screens". Locally I hit it with my FE code and it returns some data as expected. However when I deploy it to Heroku and attempt to access the endpoint with code also deployed to Heroku it returns a 404. I've tailed the Heroku logs and can see that the endpoint is hit on the server side, but the logs don't give anymore info other than it returned a 404.
I am doing other non custom route api calls and these all work fine on Heroku. I am able to auth, save the token, and hit the api with the JWT token and all other endpoints return data. This is only happening on my custom route when deployed to Heroku. I've set up cors with the appropriate origins, and I am wondering if I need to add something to my policies and middlewares in the custom route. I have verified the permissions and verified the route is accessible to authenticated users in the Strapi admin.
Here is my route:
module.exports = {
routes: [
{
method: "GET",
path: "/user-screens",
handler: "user-screens.getUserScreens",
config: {
policies: [],
middlewares: [],
},
},
],
};
And my controller:
"use strict";
/**
* A set of functions called "actions" for `user-screens`
*/
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
ctx.send(result);
});
},
};
For anyone facing this, the answer was to change how I returned the ctx response from a 'send' to a 'return' from the controller method. I am not sure why this works locally and not on Heroku, but this fixes it:
New controller code:
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
return strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
return result;
})
.catch((error) => {
return error;
});
},
};

Strapi update username from custom controller

I am trying to create a custom controller to update the user profile.
I created the routing file and the corresponding controller.
Routing file: server/src/api/profile/routes/profile.js
module.exports = {
routes: [
{
method: 'GET',
path: '/profile',
handler: 'profile.getProfile',
},
{
method: 'PUT',
path: '/profile',
handler: 'profile.updateProfile',
},
]
}
Controller: src/api/profile/controllers/profile.js
async updateProfile(ctx) {
try {
const { id } = ctx.state?.user;
const user = strapi.query('admin::user').update({
where: { id },
data: {
username: "testUsername"
}
})
ctx.body = "User updated"
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
The above code returns "User updated", but the username does not update. I am executing the PUT call with a correct Bearer authorisation token and the user permissions for that user are set to enable "updateProfile".
Oddly enough, the same code, when changed to update a different API item, works perfectly fine:
async updateArticle(ctx) {
try {
const { id } = ctx.state?.user;
const article = strapi.query('api::article.article').update({
where: { author: id },
data: {
title: "New title"
}
})
ctx.body = article
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
I am also confused by different syntaxes appearing in the official Strapi documentation, for example some docs mention:
strapi.query('admin::user').update({ id }, data)
But in other places in the documentation its:
strapi.plugins['users-permissions'].services.user.update({ id });
And then elsewhere:
strapi.query('user', 'users-permissions').update(params, values);
Another question is: do I need to sanitise the input / output in any way? If yes, how? Importing sanitizeEntity from "Strapi-utils" doesn't work, but it's mentioned in several places on the internet.
Additionally, I cannot find a list of all ctx properties. Where can I read what is the difference between ctx.body and ctx.send?
The lack of good documentation is really hindering my development. Any help with this will be greatly appreciated.

How to set the User avatar dynamically in BotFramework-WebChat based on logged in user using OAuthCard

I have developed a chat bot using Microsoft Bot Framework V4, and have used BotFramework-WebChat for providing the user to chat from website using DirectLine Token,
I am able to set the bot avatar and the user avatar by assigning the static public image URL. The problem is that I want to set the user avatar dynamically in the WebChat using below steps
Fetch the user icon using the Microsoft graph API after OAuthCard login
Set the signed in user image in the Webchat styleSetOptions dynamically.
I have gone through the Demo for setting the bot framework server and the webchat for the bot by following the samples provided
bot server == https://github.com/Microsoft/BotBuilder-Samples
webchat == https://github.com/Microsoft/BotFramework-WebChat
but there is no proper example or documentation on how to set the user image after the user has signed in. using the signed user object.
can any one point on the right direction on how can it be achieved.
Thanks in advance
You can achieve this by wrapping the Graph API call and result into the result of the AAD login process. The following code is based off of the BotBuilder-Samples 24.bot-authentication-msgraph sample and BotFramework-WebChat 17.chat-send-history sample using React.Component.
(Please be aware that the Graph sample currently located in the master branch does not include obtaining the AAD login user's photo. I have a PR which adds this feature into the sample, however I have included it here, as well. I used the WebChat sample as a reference for building the below.)
WebChat
You will need these files from sample #17, followed by the App.js file that needs altering:
public [folder]
favicon.ico
index.html
manifest.json
src [folder]
App.js
index.css
index.js
.env
package.json
App.js:
Note: I generate the direct line token locally in a separate project. This assumes an AAD profile has a photo.
import React from 'react';
import ReactWebChat, { createDirectLine, createStore} from 'botframework-webchat';
export default class extends React.Component {
constructor(props) {
super(props);
this.state = {
directLine: null,
avatarState: false, // Sets value to false; Is updated to true after login
// Sets the default styleOptions used during rendering
styleOptions: {
botAvatarImage: 'https://learn.microsoft.com/en-us/azure/bot-service/v4sdk/media/logo_bot.svg?view=azure-bot-service-4.0',
botAvatarInitials: 'BF',
userAvatarImage: 'https://github.com/compulim.png?size=64',
userAvatarInitials: 'WC'
}
};
// Creates the listener filtering for a new avatar image and applies to styleOptions
this.store = createStore(
{},
() => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
}
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY'
&& action.payload.activity.attachments
&& 0 in action.payload.activity.attachments
&& this.state.avatarState === false) {
let attachments = action.payload.activity.attachments;
if ('content' in attachments[0] && 'images' in attachments[0].content) {
this.setState(() => ({
styleOptions: {
userAvatarImage: attachments[0].content.images[0].contentUrl
},
avatarState: true
}));
}
}
return next(action);
}
)
}
componentDidMount() {
this.fetchToken();
}
async fetchToken() {
const res = await fetch('http://localhost:3979/directline/token', { method: 'POST' });
const { token } = await res.json();
this.setState(() => ({
directLine: createDirectLine({ token })
}));
}
render() {
return (
this.state.directLine ?
<ReactWebChat
className="chat"
directLine={ this.state.directLine }
styleOptions={ this.state.styleOptions }
store={ this.store }
{ ...this.props }
/>
:
<div>Connecting to bot…</div>
);
}
}
package.json
{
"name": "change-avatar",
"version": "0.1.0",
"private": true,
"homepage": "",
"dependencies": {
"botframework-webchat": ">= 0.0.0-0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-scripts": "2.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
MS Graph Bot
Update the following files in sample #24:
bot.js:
Replace async processStep with:
async processStep(step) {
// We do not need to store the token in the bot. When we need the token we can
// send another prompt. If the token is valid the user will not need to log back in.
// The token will be available in the Result property of the task.
const tokenResponse = step.result;
// If the user is authenticated the bot can use the token to make API calls.
if (tokenResponse !== undefined) {
let parts = await this.commandState.get(step.context);
if (!parts) {
parts = step.context.activity.text;
}
const command = parts.split(' ')[0].toLowerCase();
if (command === 'me') {
await OAuthHelpers.listMe(step.context, tokenResponse);
} else if (command === 'send') {
await OAuthHelpers.sendMail(step.context, tokenResponse, parts.split(' ')[1].toLowerCase());
} else if (command === 'recent') {
await OAuthHelpers.listRecentMail(step.context, tokenResponse);
} else {
let photoResponse = await OAuthHelpers.loginData(step.context, tokenResponse);
const card = CardFactory.heroCard(
`Welcome ${ photoResponse.displayName }, you are now logged in.`,
[photoResponse],
[]
);
const reply = ({ type: ActivityTypes.Message });
reply.attachments = [card];
await step.context.sendActivity(reply);
}
} else {
// Ask the user to try logging in later as they are not logged in.
await step.context.sendActivity(`We couldn't log you in. Please try again later.`);
}
return await step.endDialog();
};
oauth-helpers.js:
Add static async loginData:
/**
* Displays information about the user in the bot.
* #param {TurnContext} turnContext A TurnContext instance containing all the data needed for processing this conversation turn.
* #param {TokenResponse} tokenResponse A response that includes a user token.
*/
static async loginData(turnContext, tokenResponse) {
if (!turnContext) {
throw new Error('OAuthHelpers.loginData(): `turnContext` cannot be undefined.');
}
if (!tokenResponse) {
throw new Error('OAuthHelpers.loginData(): `tokenResponse` cannot be undefined.');
}
try {
// Pull in the data from Microsoft Graph.
const client = new SimpleGraphClient(tokenResponse.token);
const me = await client.getMe();
const photoResponse = await client.getPhoto();
// Attaches user's profile photo to the reply activity.
if (photoResponse != null) {
let replyAttachment;
const base64 = Buffer.from(photoResponse, 'binary').toString('base64');
replyAttachment = {
contentType: 'image/jpeg',
contentUrl: `data:image/jpeg;base64,${ base64 }`
};
replyAttachment.displayName = me.displayName;
return (replyAttachment);
}
} catch (error) {
throw error;
}
}
simple-graph-client.js:
Add async getPhoto:
/**
* Collects the user's photo.
*/
async getPhoto() {
return await this.graphClient
.api('/me/photo/$value')
.responseType('ArrayBuffer')
.version('beta')
.get()
.then((res) => {
return res;
})
.catch((err) => {
console.log(err);
});
}
package.json:
Be sure the #microsoft/microsoft-graph-client installs version 1.0.0 due to breaking changes around AAD 'displayName' acquisition in subsequent versions.
Once the above code was implemented, I was able to login which, upon success, immediately updated the user avatar.
Hope of help!

aws-amplify [ts] Property 'subscribe' does not exist on type '{}'. [2339]

I'm new to aws amplify. I have setup an app using amplify and I have an API that is returning records using GraphQl. I have created a subscription that is supposed to be triggered when creating a new blog entry. The entries are being created. In the documentation https://aws-amplify.github.io/docs/js/api the code samples suggest that I can use the following to subscribe to a mutation. I keep getting an error stating that error TS2339: Property 'subscribe' does not exist on type '{}'. It's coming from the assignment of client. I'm not sure why its saying this and I was hoping that you would be able to help me with this error.
import { onCreateBlog } from './graphql/subscriptions';
//GraphQl subscription
export const onCreateBlog = `subscription OnCreateBlog {
onCreateBlog {
id
name
posts {
items {
id
title
}
nextToken
}
}
}
`;
//ngInit function with an async method
ngOnInit() {
(async () => {
let client = Amplify.configure(awsmobile); // error from here
let subscription = client.subscribe(graphqlOperation(subscriptions.onCreateBlog)).subscribe({
next: data => {
console.log(data);
},
error: error => {
console.warn(error);
}
});
})();
}

How to stub a call to graphql using cypress?

I'm writing a Vue app that uses vue-apollo to interact with graphql. I'm wondering if it's possible to stub the graphql requests. I thought this should work:
it('should access a story', function() {
cy.server();
cy.route('http://localhost:3002/graphql', {
data: {
Story: { id: 2, title: 'story title', content: 'story content' }
}
});
cy.visit('/stories/2');
});
Unfortunately, I get an error from graphql complaining that id is an Int instead of an ObjectId. Am I missing something?
The problem was that stubbing fetch requests isn't yet implemented in Cypress (which is what Vue Apollo is using). I ended up following these instructions:
Install github/fetch
Add this to cypress/support/index.js:
.
Cypress.on('window:before:load', win => {
win.fetch = null;
win.Blob = null;
});
Now it works!
I got it working with this package here:
npm i #iam4x/cypress-graphql-mock
Add this line to 'support/commands.js'
import "#iam4x/cypress-graphql-mock";
go to your graphiql playground and download your schema
add task command to 'plugins/index.js' (REMEMBER TO CHANGE PATH TO SCHEMA FILE YOU DOWNLOADED EARLIER)
module.exports = (on, config) => {
on("task", {
getSchema() {
return fs.readFileSync(
path.resolve(__dirname, "../../../schema.graphql"),
"utf8"
);
}
});
};
write your tests with loaded schema
beforeEach(() => {
cy.server();
cy.task("getSchema").then(schema => {
cy.mockGraphql({
schema
});
});
});`
describe("Login Form", () => {
it("should redirect after login", () => {
cy.mockGraphqlOps({
operations: {
Login: {
login: {
jwt: "some-token",
user: {
id: "5d5a8e1e635a8b6694dd7cb0"
}
}
}
}
});
cy.visit("/login");
cy.getTestEl("email-input").type("Max Mustermann");
cy.getTestEl("password-input").type("passwort");
cy.getTestEl("submit").click();
cy.getTestEl("toolbar-title").should("exist");
});
})
Visit the original repo for further explanation as i find it less confusing. The package you have installed is just a working fork of this one:
https://github.com/tgriesser/cypress-graphql-mock

Resources