I am attempting to use firebase-functions to create a Stripe ephemeral key via a tutorial. Here is the node.js code to do so:
exports.createEphemeralKey = functions.https.onCall(async (data, context) => {
const customerId = data.customer_id;
const stripeVersion = data.stripe_version;
const uid = context.auth.uid;
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated attempt.')
throw new functions.https.HttpsError('internal', 'Illegal access attempt');
}
return stripe.ephemeralKeys.create(
{ customer: customerId },
{ stripe_version: stripeVersion }
).then((key) => {
return key
}).catch( (err) => {
functions.logger.log('Error creating ephemeral key', err)
throw new functions.https.HttpsError('internal', 'Unable to create ephemeral key: ' + err)
});
});
Immediately upon running, Xcode shows the following error code:
Error Domain=com.firebase.functions Code=13 "INTERNAL" UserInfo={NSLocalizedDescription=INTERNAL}
When I click to Manage my credit cards (which triggers the Stripe Payment Sheet), the Stripe payment sheet never loads and just shows "Loading..."
My hunch is that my Swift code is OK, and that this is a problem solely with the node.js createEphemeralKey function. I think the customerID is fine, as I can generate it with a print function in Xcode. Might this be an issue with the stripeVersion? Or something else?
Figured it out. There were two issues:
You need to use camelcase (camelCase) as opposed to snakecase (snake_case).
You need to use apiVersion as opposed to stripeVersion.
Here is the code that worked:
exports.createEphemeralKey = functions.https.onCall(async (data, context) => {
const customerId = data.customerId;
const apiVersion = data.stripeVersion;
const uid = context.auth.uid;
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated attempt.')
throw new functions.https.HttpsError('internal', 'Illegal access attempt');
}
return stripe.ephemeralKeys.create(
{ customer: customerId},
{ apiVersion: apiVersion}
).then((key) => {
return key
}).catch( (err) => {
functions.logger.log('Error creating ephemeral key', err)
throw new functions.https.HttpsError('internal', 'Unable to create ephemeral key: ' + err)
})
})
Then in Xcode remember to change your data:
let data = [
"apiVersion": apiVersion,
"customerId": UserManager.instance.user?.stripeId
]
Related
I'm trying to learn NestJs by creating a CRUD API.
I've created my controller, module, service etc...
And created a get users/id endpoint. Everything worked fine, and I decided to add some security.
I want to check if the id is not null and is a string. If not, I want to throw an exception (bad request) + console.log a message.
I also want to check if when I look for a user with a good if, the user exists. if not, throw a not found exception.
Here is my service:
async findOne(id: string): Promise<IUser | null> {
if (id === null || typeof id !== 'string') {
throw new BadRequestException('Id must be a string');
}
const user = await this.userModel.findById(id).exec();
if (user === null) {
throw new NotFoundException('No user found for this id');
}
return user;
}
and controller:
#Get(':id')
async find(#Param('id') id: string) {
try {
return await this.userService.findOne(id);
} catch (error) {
if (error instanceof BadRequestException) {
throw new HttpException(
{
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
},
HttpStatus.FORBIDDEN,
{
cause: error,
},
);
} else if (error instanceof NotFoundException) {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
error: 'This is a custom not found message',
},
HttpStatus.NOT_FOUND,
{
cause: error,
},
);
}
}
}
The problem is when I try a get request with .../users/1111 ,I got a 200 response. And when I try with a good id (a string) but with no user linked, I also get a 200 response.
I don't understand why.. Can you help me please ?
I also want to log the message.
And have you any advices ? Is the right way (standard + elegant) to do ?
Thanks guys ;)
In your code you are checking id to be of a type string and not null. Technically any param is a string, so even 1111 becomes "1111". You can verify that by logging it like so console.log({ id }) (expected result: { id: "1111" }).
For the validation I would suggest to follow the documentation on validation pipes: NestJS documentation.
TLDR;
The following code will add a global pipe to validate payloads
app.module.ts (copied from NestJS | Pipes)
import { Module } from '#nestjs/common';
import { APP_PIPE } from '#nestjs/core';
#Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
To make it work you will need to have class-validator and class-transformer installed, so run:
npm i --save class-validator class-transformer
Then declare a class that will serve as a blueprint of a DTO (Data Transfer Object), like so:
import { IsString, IsNotEmpty } from 'class-validator';
export class IdDto {
#IsNotEmpty()
#IsString()
id: string;
}
Then in your controller use the IdDto:
#Get(':id')
async find(#Param() { id }: IdDto) {
...
This already should be enough to have a basic validation. Moreover, this will convert the payload to a format that you expect (or fail and throw validation error). It is done via plainToClass method exposed from class-transformer. So there won't be any surprises with JavaScript type coercion like "1" + 1 = "11".
If you need to format your exceptions (or enrich them with additional data) you can use exception filters. There is a nice documentation about it in the official documentation.
Hope that helps!
I'm trying to create an asset report from Plaid, I'm using Cloud Functions for these calls, all the other cloud functions work (link, transactions, balance) but assetReportCreate fails and return UNAUTHENTICATED

The Plaid logs do not show the call...so I guess something is wrong in my Cloud Function, but the Google Console Logs show status 200.
has anybody experienced the same issue and know how to fix it?
PS: just double checked today to make sure I'm authenticated and I am...
//create ASSET report
exports.createAssetReport = functions.https.onCall(async (data, context) => {
const accessToken = data.accessToken;
const daysRequested = data.daysRequested;
// const options = {
// client_report_id: '123',
// webhook: "https://www.example.com", //to let you know when report is ready, get link from cloud console
// };
const configuration = new Configuration({
basePath: PlaidEnvironments[functions.config().app.env],
baseOptions: {
headers: {
"PLAID-CLIENT-ID": functions.config().plaid.client_id,
"PLAID-SECRET": functions.config().plaid.secret,
},
},
});
const plaidClient = new PlaidApi(configuration);
//call the createLinkToken METHOD of the plaidClient instance!
return plaidClient
.assetReportCreate({
access_tokens: [accessToken],
days_requested: daysRequested,
//options,
})
.then((response) => {
const assetReportId = response.data.asset_report_id;
const assetReportToken = response.data.asset_report_token;
return assetReportToken; //token is needed to retrieve the report via //pdf/get
})
.catch((err) => {
console.log(err);
throw new functions.https.HttpsError(
"internal",
" Unable to create asset report: " + err
);
});
});
I found that for whatever reason the function wasn't open for all users to invoke, adding a principal with role: "cloud functions invoker" fixed the issue, shame on you google cloud, I spent two full days on this!! jk, thanks for your services but fix these small details and you could become the #1 cloud provider
I am trying to understand the cost of Datastore. It seems that it subscribes to all Mutations. So if there are 50 users, then each message will be send 50 times, even if it not required.
As each real time mutation costs money, we will be paying unnecessary 49 times for this real time message mutation.
Also , it seems to me SyncExpression doesn't have any effect on this Subscription.
I am really stuck here. It will be great of someone can clarify
Amplify generates the datastore boilerplate code for you, but you still need to call it. You won't pay for every user and every mutation.
You will only subscribe to a mutation (explicitly call the code to listen for changes) on a per-user basis for things that user is interested in. e.g. if you are viewing a TODO item, you'd subscribe the user to that item and they'll immediately see if someone else modify it on another device.
UPDATE
Long story... I was triggering back-end computation via GraphQL by making a lambda resolver. The computation took too long and the GQL call would timeout. I updated the code so the GQL call called itself asynchronously (re-trigger the lambda), and returned immediately. Then when the long-running task completed in the spun-up lambda, I updated the a record in the database.
I update the record using AppSync instead of direct GQL so it would trigger mutations, and in the react client, I listen to a mutation for the specific record that will be updated. This way, there is just 1 user listening (if they've triggered the long running action) and that user is only notified about changes to the single DB record they're interested in, and not receiving other user's updates.
I don't know if all this is applicable to your situation. The code snippets below may help you, but they're somewhat out of context.
// In amplify/backend/api/projectname/schema.graphql
type Subscription {
onCouponWithIdUpdated(id: ID!): Coupon #aws_subscribe(mutations: ["updateCoupon"])
}
// In my useSendCoupon hook...
// Subscribe to coupon updates
useEffect(() => {
if (0 === couponId) {
return
}
console.log(`subscribe to coupon updates for couponId:`, couponId)
const onCouponWithIdUpdated = /* GraphQL */ `
subscription OnCouponWithIdUpdated($id: ID!) {
onCouponWithIdUpdated(id: $id) {
id
proofLink
owner
}
}
`
const subscription = API
.graphql(graphqlOperation(onCouponWithIdUpdated, { id: couponId }))
.subscribe({
next: ({ provider, value }) => {
const coupon = value.data.onCouponWithIdUpdated
//console.log(`Proof Link:`, coupon.proofLink)
setProofLinks([coupon.proofLink])
setSendCouponState(COUPON_STATE_PREVIEW_SUCCESS)
},
error: error => console.warn(error)
})
console.log('subscribed: ', subscription)
return () => {
console.log(`unsubscribe to coupon updates`)
subscription.unsubscribe()
}
}, [couponId])
// inside a lambda...
const updateCouponWithProof = async (authorization, couponId, proofLink) => {
const initializeClient = () => new AWSAppSyncClient({
url: process.env.API_XXXX_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.REGION,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: authorization
},
disableOffline: true,
})
const executeMutation = async (mutation, operationName, variables) => {
const client = initializeClient()
try {
const response = await client.mutate({
mutation: gql(mutation),
variables,
fetchPolicy: "no-cache",
})
return response.data[operationName]
} catch (err) {
console.log("Error while trying to mutate data", err)
throw JSON.stringify(err)
}
}
const updateCoupon = /* GraphQL */ `
mutation UpdateCoupon(
$input: UpdateCouponInput!
$condition: ModelCouponConditionInput
) {
updateCoupon(input: $input, condition: $condition) {
id
proofLink
owner
}
}
`
const variables = { input: { id: couponId, proofLink } }
try {
return await executeMutation(updateCoupon, 'updateCoupon', variables)
} catch (error) {
console.log(`executeMutation error`, error)
}
}
I am trying to update a document using a Cloud Store trigger function. Below is the code but when I try to deploy the function I get this error:
119:30 error Parsing error: Unexpected token deviceDoc
✖ 1 problem (1 error, 0 warnings)
exports.onTrxnUpdate = functions.firestore.document('/trxns/{trxnId}').onUpdate((change, context) => {
const afterData = change.after.data();
const agentId = afterData.agentId;
console.log('agentId: ', afterData.agentId);
console.log('A transaction has been updated');
/**** GET DEVICE INFORMATION ****/
const deviceDoc = db.collection('device').doc(agentId);
console.log('deviceDoc: ', deviceDoc);
if (deviceDoc == null) {
console.log('No device document found');
}
const deviceData = await deviceDoc.get(); <<<< THIS IS THE PROBLEM CODE
});
The last line is the one that is causing the error but I don't know why. I use this same line in another function and it works there.
Please help!
Thanks
You need to add async to your callback like below:
exports.onTrxnUpdate = functions.firestore.document('/trxns/{trxnId}').onUpdate(async (change, context) => {
//... rest of the code
});
I have two files, one is api.js and other one is handler.js. For schema handling I am using celebrate module #hapi/joi
On api.js I wrote only the API name
On handler.js I wrote the API functionality.
api.js
//JOI Schema Validator Middleware.
router.use(celebrate({
body: Joi.object().keys({
post: Joi.string().max(10),
userid: Joi.string(),
})
}));
const handler = require('./handler');
router.post('/createpost', handler.createPost);
router.use(errors());
module.exports = router;
By this if error happens then i got the Response like this
{"statusCode":400,"error":"Bad Request","message":"child \"post\" fails because [\"post\" length must be less than or equal to 10 characters long]","validation":{"source":"body","keys":["post"]}}
I just want to Convert this error into my own format error i.e something like this
{error: true, status: 500, message: 'validation error', version: x.x.2}
The default joi error is generated through router.use(errors()); this module. How I modify this?
Any help or suggestion is really appreciated.
TL;DR: Create your own 'errors()' function.
You have probably managed to change it by now, but just like me, I had the exact same issue and found this answerless thread.
Well, for future readers, celebrate errors() is nothing else than a function, more exactly, this one:
(err, req, res, next) => {
// If this isn't a Celebrate error, send it to the next error handler
if (!isCelebrate(err)) {
return next(err);
}
const {
joi,
meta,
} = err;
const result = {
statusCode: 400,
error: 'Bad Request',
message: joi.message,
validation: {
source: meta.source,
keys: [],
},
};
if (joi.details) {
for (let i = 0; i < joi.details.length; i += 1) {
const path = joi.details[i].path.join('.');
result.validation.keys.push(EscapeHtml(path));
}
}
return res.status(400).send(result);
}
There, you can see the response object 'result' being declared and how it's done. So, to change the output of it, you have to not use errors() and create your own function to handle it.
So, I declared a new function:
private errorHandling = (err, req, res, next) => {
if (isCelebrate(err)) {
return res.send({
statusCode: 400,
message: err.joi.message
});
}
return next(err);
}
You can obviously change the above to suit your needs.
Update
Celebrate changed their error structure to a CelebrateError, now you need access the error details using:
const errorBody = err.details.get('body'); // 'details' is a Map()
const { details: [errorDetails] } = errorBody;
instead of the err.joi. The message property remains the same.
Then, instead of using app.use(errors()) I used app.use(this.errorHandling), and now I get the celebrate response formatted as I want to.
After some research, I found out it can be solved 2 ways:
[Segments.BODY]: Joi.object().keys({
value: Joi.string().required().error(new Error('Value is required and has to be a text!')),
})
or
[Segments.BODY]: Joi.object().keys({
password: Joi.string().required().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).min(8).label('Password').messages({
'string.pattern.base': 'Your {#label} does not matche the suggested pattern',
'string.base': `Your {#label} should match the suggested pattern`,
'string.empty': `Your {#label} can not be empty`,
'string.min': `Your {#label} has to be at least {#limit} chars`,
'any.required': `Your {#label} is required`,
}),
})