Let's say we have a contact form connected with Strapi backend. Every form submit creates a new model entry and everything's fine except we need to notify administrators about new form submission.
So in api/message/model.js we add a custom lifecycle method:
module.exports = {
lifecycles: {
async afterCreate(result, data) {
await strapi.plugins["email"].services.email.send({
to: [/* Here a list of administrator email addresses should be. How to get it? */],
from: "robot#strapi.io",
subject: "New message from contact form",
text: `
Yay! We've got a new message.
User's name: ${result.name}.
Phone: ${result.phone}.
Email: ${result.email}.
Message: ${result.text}.
You can also check it out in Messages section of admin area.
`,
});
},
},
};
But I don't understand how to get administrator email addresses.
I've tried to query admins data like
console.log(
strapi.query("user"),
strapi.query("administrator"),
strapi.query("strapi_administrator")
);
But it does not work.
Ok, I got it.
The model name is strapi::user. So the whole lifecycle hook may look like
module.exports = {
lifecycles: {
async afterCreate(result, data) {
const administrators = await strapi.query("strapi::user").find({
isActive: true,
blocked: false,
"roles.code": "strapi-super-admin",
});
const emails = administrators.map((a) => a.email);
await strapi.plugins["email"].services.email.send({
to: emails,
from: "robot#strapi.io",
subject: "New message from contact form",
text: `
Yay! We've got a new message.
User's name: ${result.name}.
Phone: ${result.phone}.
Email: ${result.email}.
Message: ${result.text}.
You can also check it out in Messages section of admin area.
`,
});
},
},
};
Related
I would like to restrict my GraphQL API with User Authentication and Authorization.
All Keystone.JS documentation is talking about AdminUI authentication, which I'm not interested in at the moment.
Facts:
I want to have some social logins (no basic email/password)
I want to use JWT Bearer Tokens
Other than that you can suggest any possible way to achieve this.
My thoughts were:
I could have Firebase Authentication (which can use Google Sign-in, Apple Sign-in etc.) be done on the client-side (frontend) which would then upon successful authentication somehow connect this to my API and register user (?).
Firebase client SDK would also fetch tokens which I could validate on the server-side (?)
What is troubling is that I can't figure out how to do this in a GraphQL environment, and much less in a Keystone-wrapped GraphQL environment.
How does anyone do basic social authentication for their API made in Keystone?
Keystone authentication is independent of the Admin-UI. If you are not restricting your list with proper access control the authentication is useless. Default access is that it is open to all.
you can set default authentication at keystone level which is merged with the access control at list level.
Admin Ui Authentication
Admin UI only supports password authentication, meaning you can not go to /admin/signin page and authenticate there using other authentication mechanism. The Admin Ui is using cookie authentication. cookies are also set when you login using any other login method outside of admin-ui. This means that you can use any means of authentication outside of admin-ui and come back to admin ui and you will find yourself signed in.
Social Authentication:
Social authentication is done using passportjs and auth-passport package. there is documentation to make this work. Single Step Account Creation example is when you create user from social auth automatically without needing extra information (default is name and email). Multi Step Account Creation is when you want to capture more information like preferred username, have them accept the EULA or prompt for birthdate or gender etc.
JWT
I dont believe Keystone does pure JWT, all they do is set keystone object id in the cookie or the token is a signed version of item id (user item id) which can be decrypted only by the internal session manager using cookie secret.
Using Firebase to authenticate user
this is the flow of authentication after you create a custom mutation in keystone graphql.
client -> authenticate with Firebase -> get token -> send token to server -> server verifies the token with firebase using admin sdk -> authenticate existing user by finding the firebase id -> or create (single step) a user or reject auth call (multi step) and let client send more data like age, gender etc. and then create the user -> send token
here is the example of phone auth I did, you can also use passport based firebase package and implement your own solution.
keystone.extendGraphQLSchema({
mutations: [
{
schema: 'authenticateWithFirebase(token: String!): authenticateUserOutput',
resolver: async (obj, { token: fireToken }, context) => {
const now = Date.now();
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors || !data.firebaseUser || !data.firebaseUser.length) {
console.error(errors, `Unable to find user-authenticate`);
throw errors || new Error('unknown_user');
}
const item = data.firebaseUser[0];
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
{
schema: 'signupWithFirebase(token: String!, name: String!, email: String): authenticateUserOutput',
resolver: async (obj, { token: fireToken, name, email }, context) => {
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors) {
throw errors;
}
if (data.firebaseUser && data.firebaseUser.length) {
throw new Error('User already exist');
}
const { errors: signupErrors, data: signupData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createUser($data: UserCreateInput){
user: createUser(data: $data) {
id
name
firebaseId
email
phone
}
}`,
variables: { data: { name, phone: phone, firebaseId: uid, email, wallet: { create: { walletId: generateWalletId() } }, cart: { create: { lineItems: { disconnectAll: true } } } } },
});
if (signupErrors || !signupData.user) {
throw signupErrors ? signupErrors.message : 'error creating user';
}
const item = signupData.user;
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
],
})
I need to send a specific user ID from the bot emulator (https://github.com/microsoft/BotFramework-Emulator). I use this textbox (see on a picture below)
But nothing sent. There is absolutely another guid in activity.From.Id.
Is it possible to sent message from emulator with a specific user ID?
The short answer is, if you are using Direct Line to generate a token from a secret and you specify user Id there (see attached code), then Emulator should favor that value over any value you pass in thru the Emulator settings.
In my personal testing, the User ID setting seems to be overriding any other pre-existing value.
It should be noted, however, that if you specify a User ID value in settings, you will need to close the tabbed conversation and start it anew by re-entering the messaging endpoint and AppId/AppPassword (or reconnecting to your .bot file, if used). Simply pressing "Restart conversation" will not cause Emulator to pickup the User ID setting.
Hope of help!
// Listen for incoming requests.
server.post('/directline/token', (req, res) => {
// userId must start with `dl_` for Direct Line enhanced authentication
const userId = (req.body && req.body.id) ? req.body.id : `dl_${ Date.now() + Math.random().toString(36) }`;
const options = {
method: 'POST',
uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
headers: {
'Authorization': `Bearer ${ process.env.directLineSecret }`
},
json: {
user: {
Id: `${ userId }`
}
}
};
request.post(options, (error, response, body) => {
if (!error && response.statusCode < 300) {
res.send(body);
console.log('Someone requested a token...');
} else {
res.status(500).send('Call to retrieve token from DirectLine failed');
}
});
});
When user lands on page, the Google auth is loaded and initiated. The Google Sign-in API provides a listener function that's triggered if the user changes (GoogleAuth.currentUser.listen()). The expected behavior for the feature that I want to implement involves watching for users logging into gmails within other tabs in the browser.
const userChanged = user => {
if (user) {
console.log('CHANGED TO USER: ', user);
}
};
const loadGoogleAuth = () => {
window.gapi.load('auth2', () => {
const gapiAuth = window.gapi.auth2;
gapiAuth.init({
client_id: '<YOUR-CLIENT-ID>.apps.googleusercontent.com',
cookie_policy: 'none', //my hunch is this could be affecting listener capabilities?
scope: 'profile email openid',
});
const authInstance = gapiAuth.getAuthInstance();
const element = document.getElementById('googleSignIn');
authInstance.attachClickHandler(
element,
{},
googleUser => {
console.log(`Signed in: ${googleUser.getBasicProfile().getName()}`);
},
error => {
console.log('Sign-in error', error);
}
);
authInstance.currentUser.listen(userChanged);
});
};
const withLifeCycles = lifecycle({
componentDidMount() {
if (window.gapi) {
loadGoogleAuth();
}
},
});
This is working, but only for the first gmail that the user signs in with or the gmail that is already signed in before the user lands on the page. In other words, if I log in with Gmail A, it detects the user change but it won't detect Gmail B if I log out of Gmail A and log into Gmail B afterwards. However, it will detect relogins for Gmail A subsequently and not for any other gmail addresses. Is it possible for the listener function to detect sign-ins on any gmail address or only for the firs gmail that is detected?
You need to listen to signed-in changes too. That way you will detect that user signed out from account A and signed in to B.
auth2.isSignedIn.listen(signinChanged)
see https://developers.google.com/identity/sign-in/web/listeners
I have migrate app from parse.com to heroku with mLab and everything works fine except cloud code.
I am using Mandrill for sending email from parse cloud code which is not working with heroku
Here is what I have done so far:
Installed mandrill ~0.1.0 into parse-server-example and push the code to heroku app
Put the cloud code into '/cloud/main.js'
Called the function from iOS app which respond error as:
[Error]: Invalid function. (Code: 141, Version: 1.13.0).
Here is my code script:
Parse.Cloud.define("sendMail", function(request, response) {
var Mandrill = require('mandrill');
Mandrill.initialize('xxxxxx-xxxxx');
Mandrill.sendEmail({
message: {
text: "ffff",
subject: "hello",
from_email: "xxxxx#gmail.com",
from_name: "pqr",
to: [
{
email: "xxxxxxxxxx#gmail.com",
name: "trump"
}
]
},
async: true
},{
success: function(httpResponse) {
console.log(httpResponse);
response.success("Email sent!");
},
error: function(httpResponse) {
console.error(httpResponse);
response.error("Uh oh, something went wrong");
}
});
});
But after calling 'sendMail' function I am getting this error:
[Error]: Invalid function. (Code: 141, Version: 1.13.0).
================================== MailGun ==========================
Parse.Cloud.define('hello', function(req, res) {
var api_key = 'key-xxxxxxxxxxxxxx';
var domain = 'smtp.mailgun.org';
var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});
var data = {
from: 'xxxxxxxald#gmail.com',
to: 'xxxxx8#gmail.com',
subject: 'Hello',
text: 'Testing some Mailgun awesomness!'
};
mailgun.messages().send(data, function (error, body) {
console.log(body);
});
//res.success(req.params.name);
});
I had a similar problem with sendgrid, but I finally find a way around the problem.
I think this steps may help you,
Miss some brackets or some code separator? ( try rewritting the entire code in the main.js )
The app is actually running? ( when you type "heroku open" in the terminal you get the default message? ) - if not check step 1.
If the previous are not working, rollback to a safe build and Add the add-ons in the heroku dashboard instead of installing them yourself, then download the git and do any changes to git and then push.
Below I have pasted from cloud code main.js code that is working using Mandrill on heroku parse application to send password recovery e-mail.
in cloud code main.js:
var mandrill_key = process.env.MANDRILL_KEY;
var Mandrill = require('mandrill-api/mandrill');
var mandrill_client = new Mandrill.Mandrill(mandrill_key);
{
success: function(gameScore) {
//alert('New object created with objectId: ' + gameScore.id);
mandrill_client.messages.send(
{
message: {
html: "<p>Hello " + firstUser.get('fullname') + ",</p><p>We received your request to reset your password.</p><p>Your user name is <strong>" + firstUser.get('username') + "</strong>. Please click here to create a new password. This link will expire in one hour after this mail was sent</p><p>If you need additional help, just let us know.</p><p>SampleCompany Support<br>customerservice#example.com</p><p>Copyright Sample Company, Inc. 2014-2017</p>",
subject: "Sample Company Name account recovery",
from_email: "customerservice#example.com",
from_name: "Sample Company Name",
to: [
{
email: firstUser.get('email'),
name: firstUser.get('fullname')
}
]
},
async: true
},
//Success
function(httpResponse) {
console.log(httpResponse);
//alert("Email sent!");
},
//Failure
function(httpResponse) {
console.error(httpResponse);
//alert("Uh oh, something went wrong");
});
},
error: function(gameScore, error) {
console.error(error.message);
//alert('Failed to create new object, with error code: ' + error.message);
},
useMasterKey: true
})
I'm using Parse Cloud Code.
My system has a welcome message. I use MailGun to send it.
The problem I have is that the message now is an HTML file, so I would like to let the HTML file in my server, read it using Cloud Code and pass that info to MailGun.
Can I read a local text file using Cloud Code and have it in my program as a string?
Should I save that file in my public folder or in the same folder than my cloudcode program?
I'm not confident with MailGun, but I believe it should work like MailChimp or Mandrill. If so, you should be able to store on MailGun your whole HTML template and just have some template_vars to complete.
This is a sample code of our own way to send mail with HTML thanks to the Mandrill system
Parse.Cloud.define("sendMailTemplate", function(request, response) {
var emails = request.params.emails;
var template_name = request.params.template_name;
var template_merge_content = request.params.template_merge_content;
var subject = request.params.subject;
var Mandrill = require('cloud/mandrillTemplateSend.js');
if (subject === undefined) {
subject = 'Mail sent by Mandrill';
body = subject;
}
Parse.Config.get().then(function(config) {
Mandrill.initialize(config.get('Mandrill_key'));
}).then(function() {
_.each(emails, function(email) {
Mandrill.sendTemplate({
template_name: template_name,
template_content: [{
name: template_merge_content.username,
content: ''
}],
message: {
text: '',
subject: subject,
from_email: 'contact#yourdomain.com',
to: [{
email: email,
name: template_merge_content.username
}],
merge_vars: [{
rcpt: email,
vars: template_merge_content
}],
},
async: false
});
});
}).then(function() {
response.success('Success');
}, function(error) {
response.error(error);
});
});
This object template_merge_content is quiet important. It's an object where is saved all the dynamic vars which are send to complete your HTML mail.
According to http://blog.mailgun.com/transactional-html-email-templates/ it seems you have same kind of method to send your mail.
So final advice would be to NOT store your HTML template within any Parse's class, or to save it within https://parse.com/docs/js/api/symbols/Parse.Config.html