Firebase Set the Notification Title and Body to Database Value - node.js

I think this is a fairly basic question, but I have a Firebase function file with an index.js file within. I am able to trigger a push notification when the database is updated with a new value, but just need help setting the notification's title and body to the values added in the child nodes. Here is what I have so far:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// database tree
exports.sendPushNotification = functions.database.ref('/questions/{id}').onWrite(event =>{
const payload = {
notification: {
title: //Value at /questions/{id}/question,
body: //Value at /questions/{id}/bestAnswer,
badge: '1',
sound: 'default',
}
};
return admin.database().ref('fcmToken').once('value').then(allToken => {
if (allToken.val()){
const token = Object.keys(allToken.val());
console.log('token? ${token}');
return admin.messaging().sendToDevice(token, payload).then(response =>{
return null;
});
}
return null;
});
});

You can reference any value of the created object:
event.val().question
event.val().bestAnswer
Using those you can update your notification:
payload.notification.title = event.val().question;
payload.notification.body = event.val().bestAnswer;
or
const payload = {
notification: {
title: event.val().question,
body: event.val().bestAnswer,
badge: '1',
sound: 'default',
}

Related

Firebase 401 Error https://fcm.googleapis.com/fcm/send works with admin().messaging().send() but does not work with admin().messaing().sendTopic()

I have created my backend in nodejs like shown below. When calling code example 1, code executes without problem. Example 1 calls admin().messaging().send() and works; however, when calling code example 2 (sending topic message) there is 401 error (like shown below)
An error occurred when trying to authenticate to the FCM servers.
Make sure the credential used to authenticate this SDK has the proper permissions.
See https://firebase.google.com/docs/admin/setup for setup instructions.
PROJECT_NOT_PERMITTED
Error 401
Is there authorization setting when sending topic message? What needs to be done to resolve issue with 401 Error? Thank you
Example 1
// Load the AWS SDK for Node.js
var AWS = require("aws-sdk");
// Set the region
AWS.config.update({ region: "ap-northeast-2" });
var admin = require("firebase-admin");
var serviceAccount = require("../firebadeCredentialInformation.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
module.exports.handler = async (event) => {
console.log(event);
var body = JSON.parse(event.body);
console.log(body);
var topic;
var topicPayload;
try {
//body, title, data, token
const message = {
data: {
type: "VOTE",
senderName: body.senderName,
question: body.question,
questionRangeKey: body.questionRangeKey,
senderToken: body.senderToken,
gender: body.gender,
schoolGrade: body.schoolGrade,
schoolName: body.schoolName,
},
token: body.token,
};
//? Make sure the message is sent
//TODO REMOVE BELOW AWAIT TO IMPROVE SPEED
var result = await admin.messaging().send(message);
console.log(result);
return {
statusCode: 200,
body: JSON.stringify(
{
message: "Sucessfully sent response",
input: event,
},
null,
2
),
};
} catch (e) {
console.log("Error", e);
return {
statusCode: 500,
body: JSON.stringify(
{
message: e,
input: event,
},
null,
2
),
};
}
};
Example 2
// Load the AWS SDK for Node.js
var AWS = require("aws-sdk");
// Set the region
AWS.config.update({ region: "ap-northeast-2" });
var admin = require("firebase-admin");
var serviceAccount = require("../firebadeCredentialInformation.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
module.exports.handler = async (event) => {
console.log(event);
var body = JSON.parse(event.body);
console.log(body);
var topic;
var topicPayload;
try {
//body, title, data, token
const message = {
data: {
type: "VOTE",
senderName: body.senderName,
question: body.question,
questionRangeKey: body.questionRangeKey,
senderToken: body.senderToken,
gender: body.gender,
schoolGrade: body.schoolGrade,
schoolName: body.schoolName,
},
token: body.token,
};
//? Make sure the message is sent
//TODO REMOVE BELOW AWAIT TO IMPROVE SPEED
var result = await admin.messaging().send(message);
console.log(result);
if (body.schoolId != "") {
//* Ff not missing school id
const topicPayload = {
data: {
type: "TOPIC",
senderName: body.senderName,
question: body.question,
questionRangeKey: body.questionRangeKey,
senderToken: body.senderToken,
gender: body.gender,
schoolGrade: body.schoolGrade,
schoolName: body.schoolName,
},
};
const schoolId = body.schoolId;
const topic = "/topics/" + schoolId;
console.log(topic);
//? Make sure the message is sent
//TODO REMOVE BELOW AWAIT TO IMPROVE SPEED
// Send a message to devices subscribed to the provided topic.
var topicResult = await admin
.messaging()
.sendToTopic(topic, topicPayload);
console.log(topicResult);
}
return {
statusCode: 200,
body: JSON.stringify(
{
message: "Sucessfully sent response",
input: event,
},
null,
2
),
};
} catch (e) {
console.log("Error", e);
return {
statusCode: 500,
body: JSON.stringify(
{
message: e,
input: event,
},
null,
2
),
};
}
};
Have figured out the issue for those who are stuck as well. Instead of using admin().message.sendTopic(). I have used the command below which is also same as sending a topic message, which worked.
//* if not missing school id
const schoolId = body.schoolId;
const topic = "/topics/" + schoolId;
const topicPayload = {
data: {
type: "TOPIC",
senderName: body.senderName,
},
topic: topic,
};
console.log(topic);
// Send a message to devices subscribed to the provided topic.
var topicResult = await admin.messaging().send(topicPayload);

createClickwrap returns 404 Not Found

When I call createClickwrap method, I got 404 error Not Found. If I run this method through Quickstart generated demo project, I don't get this error. However, if I run it through my project I get the error. If I debug the demo app and my app, the functions parameters are the same.
This is the code in my app:
docusign controller:
const docuSignService = require('./docusign_esign_service');
const demoDocumentsPath = path.resolve(__dirname, '../demo_documents');
const { createClickwrap } = require('./createClickWrap');
async getDocusignRecieptService() {
const authResponse = await docuSignService.authenticate();
if (authResponse) {
const docTermsPdf = 'Term_Of_Service.pdf';
const docFile = path.resolve(demoDocumentsPath, docTermsPdf);
const { basePath, accessToken, apiAccountId } = authResponse;
const { clickwrapId } = await createClickwrap({ docFile, basePath, accessToken, accountId: apiAccountId });
const res = await activateClickwrap({ clickwrapId, basePath, accessToken, accountId: apiAccountId });
console.log({ res });
}
}
docuSignService.js
const SCOPES = ['signature', 'impersonation', 'openid', 'click.manage', 'click.send'];
const fs = require('fs');
const docusign = require('docusign-esign');
class DocusingService {
async authenticate() {
const jwtLifeSec = 10 * 60, // requested lifetime for the JWT is 10 min
dsApi = new docusign.ApiClient();
dsApi.setOAuthBasePath(process.env.dsOauthServer.replace('https://', '')); // it should be domain only.
let rsaKey = fs.readFileSync(process.env.privateKeyLocation);
try {
const results = await dsApi.requestJWTUserToken(
process.env.dsJWTClientId,
process.env.impersonatedUserGuid,
SCOPES,
rsaKey,
jwtLifeSec
);
const accessToken = results.body.access_token;
// get user info
const userInfoResults = await dsApi.getUserInfo(accessToken);
// use the default account
let userInfo = userInfoResults.accounts.find((account) => account.isDefault === 'true');
return {
accessToken: results.body.access_token,
apiAccountId: userInfo.accountId,
basePath: `${userInfo.baseUri}/restapi`
};
} catch (e) {
let body = e.response && e.response.body;
// Determine the source of the error
if (body) {
// The user needs to grant consent
if (body.error && body.error === 'consent_required') {
if (this.getConsent()) {
return this.authenticate();
}
} else {
// Consent has been granted. Show status code for DocuSign API error
this._debug_log(`\nAPI problem: Status code ${e.response.status}, message body:
${JSON.stringify(body, null, 4)}\n\n`);
}
}
}
}
getConsent() {
var urlScopes = SCOPES.join('+');
// Construct consent URL
var redirectUri = 'https://developers.docusign.com/platform/auth/consent';
var consentUrl =
`${process.env.dsOauthServer}/oauth/auth?response_type=code&` +
`scope=${urlScopes}&client_id=${process.env.dsJWTClientId}&` +
`redirect_uri=${redirectUri}`;
throw new Error(`Open the following URL in your browser to grant consent to the application: ${consentUrl}`);
}
getArgs(apiAccountId, accessToken, basePath, signerEmail, signerName, id, agreementData, redirect_uri) {
const envelopeArgs = {
signerEmail: signerEmail,
signerName: signerName,
status: 'sent',
signerClientId: id,
dsReturnUrl: redirect_uri,
agreement: agreementData
};
const args = {
accessToken: accessToken,
basePath: basePath,
accountId: apiAccountId,
envelopeArgs: envelopeArgs
};
return args;
}
}
module.exports = new DocusingService();
createClickWrap.js
const createClickwrap = async ({ docFile, clickwrapName = 'clickwrapName', basePath, accessToken, accountId }) => {
// Step 3. Construct the request Body
// Create display settings model
const displaySettings = docusignClick.DisplaySettings.constructFromObject({
consentButtonText: 'I Agree',
displayName: 'Terms of Service',
downloadable: true,
format: 'modal',
hasAccept: true,
mustRead: true,
requireAccept: true,
documentDisplay: 'document'
});
// Create document model
// Read and encode file. Put encoded value to Document entity.
// The reads could raise an exception if the file is not available!
const documentPdfExample = fs.readFileSync(docFile);
const encodedExampleDocument = Buffer.from(documentPdfExample).toString('base64');
const document = docusignClick.Document.constructFromObject({
documentBase64: encodedExampleDocument,
documentName: 'Terms of Service',
fileExtension: 'pdf',
order: 0
});
// Create clickwrapRequest model
const clickwrapRequest = docusignClick.ClickwrapRequest.constructFromObject({
displaySettings,
documents: [document],
name: clickwrapName,
requireReacceptance: true
});
// Step 4. Call the Click API
const dsApiClient = new docusignClick.ApiClient();
dsApiClient.setBasePath(basePath);
dsApiClient.addDefaultHeader('Authorization', 'Bearer ' + accessToken);
const accountApi = new docusignClick.AccountsApi(dsApiClient);
// Create a clickwrap
let result = null;
try {
result = await accountApi.createClickwrap(accountId, {
clickwrapRequest
});
} catch (e) {
debugger;
console.log(e);
}
debugger;
console.log(`Clickwrap was created. ClickwrapId ${result.clickwrapId}`);
return result;
};
module.exports = { createClickwrap };
Parameters look like this in the demo app and it works:
and these are the parameters in my app:
The first parameter accountId is the same. Why I am getting this issue in my app if function gets the same parameters?
"Error: Not Found
at Request.callback (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:696:15)
at IncomingMessage.<anonymous> (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:906:18)
at IncomingMessage.emit (node:events:539:35)
at IncomingMessage.emit (node:domain:475:12)
at endReadableNT (node:internal/streams/readable:1345:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21)"
Thanks to pointing out in the comments, when I changed basePath ${userInfo.baseUri}/restapi to ${userInfo.baseUri}/clickapi, it works now.

is there a way i can handle file upload in react

iam trying to submit these fields to node server from react i can submit rest of the input fields and select options but i cannot upload the photo plus i can upload all fields (including the photo) to the server using POSTMAN
// fields to submit to node server
state = {
data: {
fullName: '',
email: '',
phoneNumber: '',
branchId: '',
jobId: '',
salary: '',
image: '',
// startDate: '',
},
// onChange handler
handleChange = ({ currentTarget: input }) => {
//clone the errors
const errors = { ...this.state.errors }
//validate the input
const errorMessage = this.validateProperty(input)
//
if (errorMessage) errors[input.name] = errorMessage
//
else delete errors[input.name]
//clone the data
const data = { ...this.state.data }
//override the state data with the input value
data[input.name] = input.value
// update the state with the data and errors
this.setState({ data, errors })
}
// submit or onClick handler
doSubmit = async () => {
const { data } = this.state
const fd = new FormData()
fd.append('fullName', data.fullName)
fd.append('email', data.email)
fd.append('phoneNumber', data.phoneNumber)
fd.append('branchId', data.branchId)
fd.append('jobId', data.jobId)
fd.append('salary', data.salary)
fd.append('image', data.image)
// fd.append()
await saveEmployee(fd)
this.props.history.push('/employees')
}
I'm guessing that the image input is a file field?
You are now storing the input#value of the file field but you're actually looking for the input#files[0] value. This contains the reference to the actual file and can be appended to the form.
handleChange = ({ currentTarget: input }) => {
//clone the errors
const errors = { ...this.state.errors }
//validate the input
const errorMessage = this.validateProperty(input)
//
if (errorMessage) errors[input.name] = errorMessage
//
else delete errors[input.name]
//clone the data
const data = { ...this.state.data }
//override the state data with the input value
data[input.name] = input.type === 'file' ? input.files[0] : input.value
// update the state with the data and errors
this.setState({ data, errors })
}
This can be achieved in the following way:
In Server use "multer" to handle uploaded file:
https://expressjs.com/en/resources/middleware/multer.html
Now create an endpoint to handle your file upload.
For instance(server code):
// Import Multer
const multer = require("multer");
// Express
let app = express();
// upload destination
const uploadLocation = multer({ dest: "uploads/" });
// Endpoint
app.post(
"/upload",
// Here pass the name of file that you've set in FormData. In your
// case its "image", as your appending file data as image in your
// code.
uploadLocation.single("image"),
async (req, res) => {
// get file from req
const file = req.file;
// This is your uploaded file
console.log(file);
// You can handle other data here...
res.status(200).send({ path: `uploads/${file.filename}` });
}
);
Now use this endpoint to send your from data.
For instance (client side code in Javascript):
const data = await fetch(
`${process.env.REACT_APP_SERVER_URL}/upload`,
{
method: "POST",
credentials: "include",
headers: {},
// Send your form data like this to server
body: formData,
}
);
const { path } = await data.json();
// This is your path of uploaded file
console.log(path)

flutter: subscribing user to different topics in one collection for push notifications fcm

i wanna subscribe my users to different "language" topics according to their preference so they receive notifications in the language they want
in my firestore i have a collection called notifications inside its document(default) i have two more collections .. english and arabic.
now in my shopping app i subbed the user to english topic to test if it works
_fcm.subscribeToTopic('english');
and here's my index.js code for cloud fuctions:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var newData;
exports.messageTrigger = functions.firestore.document('notifications/default/{languageId}/{messagesId}').onCreate(async (snapshot, context) => {
newData = snapshot.data();
const payload = {
notification: {
title: newData.message,
body: newData.body,
},
data: {
click_action: 'FLUTTER_NOTIFICATION_CLICK',
message: newData.message,
}
};
if (context.param.languageId === "english") {
await admin.messaging().sendToTopic('english', payload);
}
else if (context.param.languageId=== "arabic") {
await admin.messaging().sendToTopic('arabic', payload);
}
});
but when i create a document in the english collection inside notifications collection it doesnt work. anybody know why?
i fixed it by making my functions like this:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var newData;
exports.messageTrigger = functions.firestore.document('notifications/{notificationsId}').onCreate(async (snapshot, context) => {
newData = snapshot.data();
const payload = {
notification: {
title: newData.message,
body: newData.body,
},
data: {
click_action: 'FLUTTER_NOTIFICATION_CLICK',
message: newData.message,
}
};
if (newData.language === 'english'){
await admin.messaging().sendToTopic('english', payload);
}else if (newData.language === 'arabic'){
await admin.messaging().sendToTopic('arabic', payload);
}
});

Domain-wide delegation using default credentials in Google Cloud Run

I'm using a custom service account (using --service-account parameter in the deploy command). That service account has domain-wide delegation enabled and it's installed in the G Apps Admin panel.
I tried this code:
app.get('/test', async (req, res) => {
const auth = new google.auth.GoogleAuth()
const gmailClient = google.gmail({ version: 'v1' })
const { data } = await gmailClient.users.labels.list({ auth, userId: 'user#domain.com' })
return res.json(data).end()
})
It works if I run it on my machine (having the GOOGLE_APPLICATION_CREDENTIALS env var setted to the path of the same service account that is assigned to the Cloud Run service) but when it's running in Cloud Run, I get this response:
{
"code" : 400,
"errors" : [ {
"domain" : "global",
"message" : "Bad Request",
"reason" : "failedPrecondition"
} ],
"message" : "Bad Request"
}
I saw this solution for this same issue, but it's for Python and I don't know how to replicate that behaviour with the Node library.
After some days of research, I finally got a working solution (porting the Python implementation):
async function getGoogleCredentials(subject: string, scopes: string[]): Promise<JWT | OAuth2Client> {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
})
const authClient = await auth.getClient()
if (authClient instanceof JWT) {
return (await new google.auth.GoogleAuth({ scopes, clientOptions: { subject } }).getClient()) as JWT
} else if (authClient instanceof Compute) {
const serviceAccountEmail = (await auth.getCredentials()).client_email
const unpaddedB64encode = (input: string) =>
Buffer.from(input)
.toString('base64')
.replace(/=*$/, '')
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://accounts.google.com/o/oauth2/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: subject,
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT',
})
const iamPayload = `${unpaddedB64encode(header)}.${unpaddedB64encode(payload)}`
const iam = google.iam('v1')
const { data } = await iam.projects.serviceAccounts.signBlob({
auth: authClient,
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
requestBody: {
bytesToSign: unpaddedB64encode(iamPayload),
},
})
const assertion = `${iamPayload}.${data.signature!.replace(/=*$/, '')}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({ assertion, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer' })
const response = await fetch('https://accounts.google.com/o/oauth2/token', { method: 'POST', headers, body }).then(r => r.json())
const newCredentials = new OAuth2Client()
newCredentials.setCredentials({ access_token: response.access_token })
return newCredentials
} else {
throw new Error('Unexpected authentication type')
}
}
What you can do here is define ENV variables in your yaml file as described in this documentation to set the GOOGLE_APPLICATION_CREDENTIALS to the path of the JSON key.
Then use a code such as the one mentioned here.
const authCloudExplicit = async ({projectId, keyFilename}) => {
// [START auth_cloud_explicit]
// Imports the Google Cloud client library.
const {Storage} = require('#google-cloud/storage');
// Instantiates a client. Explicitly use service account credentials by
// specifying the private key file. All clients in google-cloud-node have this
// helper, see https://github.com/GoogleCloudPlatform/google-cloud-node/blob/master/docs/authentication.md
// const projectId = 'project-id'
// const keyFilename = '/path/to/keyfile.json'
const storage = new Storage({projectId, keyFilename});
// Makes an authenticated API request.
try {
const [buckets] = await storage.getBuckets();
console.log('Buckets:');
buckets.forEach(bucket => {
console.log(bucket.name);
});
} catch (err) {
console.error('ERROR:', err);
}
// [END auth_cloud_explicit]
};
Or follow an approach similar to the one mentioned here.
'use strict';
const {auth, Compute} = require('google-auth-library');
async function main() {
const client = new Compute({
serviceAccountEmail: 'some-service-account#example.com',
});
const projectId = await auth.getProjectId();
const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({url});
console.log(res.data);
}
main().catch(console.error);

Resources