Unimplemented API error when running admin.storage().bucket(..).file(..).move() in an emulated Firebase function - firebase

"firebase": "9.0.1" - local
"firebase-tools": 9.17.0 - global
"node": v14.17.4
I'm calling .move() using the Firebase admin SDK in an emulated function. Firebase returns the following error:
ApiError: file#copy failed with an error - Not Implemented
at new ApiError (.../functions/node_modules/#google-cloud/common/build/src/util.js:73:15)
statusCode: 501
request: {
agent: [Agent],
headers: [Object],
href: 'http://localhost:9199/b/public-8s9ch/o/91ff38b1-b521-4a23-8844-b12cffa0ee98%2Fscreenshot.png/rewriteTo/b/private-8s9ch/o/91ff38b1-b521-4a23-8844-b12cffa0ee98%2Fcd665e29-edc8-4832-9bc7-110c21f1e560?'
},
body: 'Not Implemented',
The exact same code, with the same tests and installed packages, work perfectly when deployed to Firebase instead of being used in the emulator.
The same issue occurs with any call to the storage API regardless of using the Admin SDK or using the client SDK e.g. .copy, .move, .delete, etc.
Here's some code to help with reproducing the error:
export const firebaseFunction = functions
.runWith({ memory: "128MB" })
.region("us-central1")
.firestore.document("documents/{docId}")
.onCreate(async (snap, context) => {
try {
const { srcId } = snap.data();
const [files] = await admin
.storage()
.bucket('public')
.getFiles({
prefix: `${srcId}/`,
delimiter: "/",
autoPaginate: false,
});
const promises = files.map((file) => {
return new Promise<void>(async (resolve, reject) => {
try {
const dst = admin
.storage()
.bucket('private')
.file(`${srcId}/${uuidv4()}`);
await file.move(dst);
resolve();
} catch (err) {
reject(err);
}
});
});
await Promise.all(promises);
} catch (error) {
return console.log(error);
}
});

Related

Signin method with Nextjs and trpc returning resolver is not a function

So im trying to build my register method without re-enventing nothing crazy with the create-t3-app stack with nextjs, trpc and nextauth:
export const signUpRouter = router({
signup: publicProcedure.input(UserModel).mutation(async ({ ctx, input }) => {
debugger;
try {
const { nickname, email, password } = input;
//check duplicate users
const checkingUsers = await ctx.prisma.user.findFirst({
where: {
email,
},
});
if (checkingUsers) {
return { message: "User already exists..." };
}
//hash password
return await ctx.prisma.user.create({
data: {
nickname,
email,
password: await hash(password, 12),
},
});
} catch (error: any) {
console.log("error", error);
throw new Error(error);
}
}),
});
export default signUpRouter;
This file is inside pages/api/auth/signup.ts
Should I have this on the server part ?
I have the router on my appRouter file
export const appRouter = router({
userLogin: userLoginRouter,
auth: authRouter,
signin: signInRouter,
signup: signUpRouter,
});
And when clicking on the register button:
async function onSumitRegisterValues(values: any) {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
};
await fetch("http://localhost:3000/api/auth/signup", options)
.then((res) => res.json())
.then((data) => {
if (data?.ok) router.push("http://localhost:3000");
});
}
The values form contains nickname, email, password and cpassword to confirm password.
im getting a 500 on post
Server Error
TypeError: resolver is not a function
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Maybe its my lack of knowledge with trpc and next but ngl, its making me want to separate my backend into something different. But since im not rushing in building this project i really want to try to figure out what i shoud be doing better.
Why are you using fetch instead of using your useQuery method from trpc? The whole point of trpc is that you can skip fetch and will also have type safety.
https://trpc.io/docs/useQuery

NEXT.JS – Using aws-sdk to upload to DigitalOceans - dev mode work, prod isn't

I'm using aws-sdk to upload images to DigitalOceans bucket. On localhost it works 100% but production seems like the function goes on without an error but the file does not upload to the bucket.
I cannot figure out what is going on and can't think of a way to debug this. tried aswell executing the POST request with Postman multipart/form-data + adding file to the body of the request and it is the same for localhost, working, and production is not.
my api endpoint:
import AWS from 'aws-sdk'
import formidable from "formidable"
import fs from 'fs'
const s3Client = new AWS.S3({
endpoint: process.env.DO_SPACES_URL,
region: 'fra1',
credentials: {
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET
}
})
export const config = {
api: {
bodyParser: false
}
}
export default async function uploadFile(req, res) {
const { method } = req
const form = formidable()
const now = new Date()
const fileGenericName = `${now.getTime()}`
const allowedFileTypes = ['jpg', 'jpeg', 'png', 'webp']
switch (method) {
case "POST":
try {
form.parse(req, async (err, fields, files) => {
const fileType = files.file?.originalFilename?.split('.').pop().toLowerCase()
if (!files.file) {
return res.status(400).json({
status: 400,
message: 'no files'
})
}
if (allowedFileTypes.indexOf(fileType) === -1) {
return res.status(400).json({
message: 'bad file type'
})
}
const fileName = `${fileGenericName}.${fileType}`
try {
s3Client.putObject({
Bucket: process.env.DO_SPACES_BUCKET,
Key: `${fileName}`,
Body: fs.createReadStream(files.file.filepath),
ACL: "public-read"
}, (err, data) => {
console.log(err)
console.log(data)
})
const url = `${process.env.FILE_URL}/${fileName}`
return res.status(200).json({ url })
} catch (error) {
console.log(error)
throw new Error('Error Occured While Uploading File')
}
});
return res.status(200)
} catch (error) {
console.log(error)
return res.status(500).end()
}
default:
return res.status(405).end('Method is not allowed')
}
}

Trying to initialize test environment with v9 firestore sdk using jest

Im trying to set up my testing environment to test my security fules with firestore. I've copied this code from https://firebase.google.com/docs/rules/unit-tests#before_you_run_the_emulator
let testEnv : RulesTestEnvironment;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: "demo-project-1234",
firestore: {
rules: fs.readFileSync('firestore.rules', 'utf8'),
},
});
});
However, I'm getting this error.
The host and port of the firestore emulator must be specified. (You may wrap the test script with firebase emulators:exec './your-test-script' to enable automatic discovery, or specify manually via initializeTestEnvironment({firestore: {host, port}}).
Anyone know how to solve this?
EDIT
I tried adding host and port to my running emulator like so
let testEnv : RulesTestEnvironment;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: "comment-section-e9c09",
firestore: {
rules: fs.readFileSync('firestore.rules', 'utf8'),
host:'localhost',
port:8080
},
});
});
Now it seems to be able to connect to my emulator, but when I try to fx clear the database like
test("sefse", () => {
testEnv.clearDatabase()
})
I get the following error
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Error: The host and port of the database emulator must be specified. (You may wrap the test script with 'firebase emulators:exec './your-test-script'' to enable automatic discovery, or specify manually via initializeTestEnvironment({database: {host, port}}).".] {
I give u a "mocha-based" starting point:
security.test.js:
import { readFileSync } from 'fs';
import { assertFails, assertSucceeds, initializeTestEnvironment } from "#firebase/rules-unit-testing";
import { doc, getDoc, setDoc } from "firebase/firestore";
let testEnv;
let unauthedDb;
describe("general database behaviour", () => {
before(async () => {
testEnv = await initializeTestEnvironment({
projectId: "demo-project-1234",
firestore: {
rules: readFileSync("firestore.rules", "utf8"),
host: "127.0.0.1",
port: "8080"
},
});
unauthedDb = testEnv.unauthenticatedContext().firestore();
});
after(async () => {
await testEnv.cleanup();
});
it("should let read anyone the database", async () => {
await testEnv.withSecurityRulesDisabled(async (context) => {
await setDoc(doc(context.firestore(), 'data/foobar'), { foo: 'bar' });
});
await assertSucceeds(getDoc(doc(unauthedDb, 'data/foobar')))
})
it("should not allow writing the database", async () => {
await assertFails(setDoc(doc(unauthedDb, '/data/foobar'), { something: "something" }))
})
})
Did you specify the firestore port and rules path in your firebase.json file like so?
"emulators": {
"firestore": {
"port": 8080
}
},
"firestore": {
"rules": "./rules/firestore.rules"
}

Nodemailer in vercel not sending email in production

I'm using Nodemailer to send emails in my serverless Next.js project, deployed in Vercel, which works perfectly in development mode. But I'm having problems in production. No error returned, everything works the same way as is development mode, except I don't receive any email.
I have another project built with React and deployed in Heroku where I send emails the same way and it works fine, development and production, so I understand the problem is with Vercel.
Yes, I enabled "Allow Less Secured Apps" in Google account and yes, I enabled Captcha.
I also read this https://vercel.com/docs/solutions/email but it doesn't really make me understand what I should do in my case. I can see it's a matter of SMTP but I don't know what exactly.
Anybody experienced this kind of problem? How can I fix this?
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
auth: {
user: myEmail#gmail.com,
pass: myEmailPass
}
});
const mailOptions = {
from: `${req.body.name} ${req.body.email}`,
to: myEmail#gmail.com,
subject: `${req.body.subject}`,
text: `Text: ${req.body.text}`
}
transporter.sendMail(mailOptions, (err, res) => {
if(err) {
console.log(err);
} else {
console.log("success");
}
});
UPDATE
I changed to SendGrid: made an account, created an API Key, and changed the code like so(instead the one above):
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: `myEmail#gmail.com`,
from: `myEmail#gmail.com`,
subject: `${req.body.subject}`,
text: `${req.body.text}`
};
sgMail
.send(msg)
.then(() => {
console.log('email sent')
})
.catch((error) => {
console.error("error", error)
});
It logs out "email sent" but I don't receive any email.
It's the same problem like with Nodemailer.
I'm confused now...
I ran into this issue and managed to fix it and keep using nodemailer by adding in promises with async/await.
const nodemailer = require("nodemailer");
export default async (req, res) => {
const { firstName, lastName, email, message } = JSON.parse(req.body);
const transporter = nodemailer.createTransport({
port: 465,
host: "smtp.gmail.com",
auth: {
user: "myEmail#gmail.com",
pass: "password",
},
secure: true,
});
await new Promise((resolve, reject) => {
// verify connection configuration
transporter.verify(function (error, success) {
if (error) {
console.log(error);
reject(error);
} else {
console.log("Server is ready to take our messages");
resolve(success);
}
});
});
const mailData = {
from: {
name: `${firstName} ${lastName}`,
address: "myEmail#gmail.com",
},
replyTo: email,
to: "recipient#gmail.com",
subject: `form message`,
text: message,
html: `${message}`,
};
await new Promise((resolve, reject) => {
// send mail
transporter.sendMail(mailData, (err, info) => {
if (err) {
console.error(err);
reject(err);
} else {
console.log(info);
resolve(info);
}
});
});
res.status(200).json({ status: "OK" });
};
This problem is really confusing indeed. I've managed to fix this by simply adding async/await. This is because streaming responses (fire-and-forget functions) are not supported by Vercel.
Source: https://vercel.com/docs/platform/limits#streaming-responses
I have already encountered the same problem, nodemailer was not working on vercel but on heroku everything worked perfectly. it is specified in the doc that vercel does not block stmp connections but according to what I have experienced, in practice stmp connections are blocked. what you can do is use an alternative to nodemailer. use sendgrid and it works fine
An article on how integrating Sendgrid with Next.js
I had a similar issue with Nodemailer but I fixed it by first adding the environment variables in Vercel then commit to the github(It will automatically be uploaded on vercel). So add the variables to vercel first for it to take effect
In my own case, wrapping my email function with async solved it for me.
eg:
const sendMessage = async(message)=>{
await transporter.sendMail({...options here})
}
Then in my API I called my function using:
await sendMessage('your message')
I tried all the async/await responses and didn't work at the beginning. Digging through the real time functions logs of the app, I noticed that there was an Error: Missing credentials for "PLAIN", so all I had to do was add the respective .env variables to vercel environment variables and it worked. Here's the complete code though:
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = any
const nodemailer = require('nodemailer')
const auth = {
user: process.env.WEB_MAILER,
pass: process.env.WEB_MAILER_PASSWORD,
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const { name, email, subject, message } = req.body
const mailData = {
to: process.env.EMAIL_TO,
from: process.env.WEB_MAILER,
name: name,
subject: subject,
text: `Email: ${email}.\n\nMessage: ${message}`,
html: `<div>Email: ${email}.\n\nMessage: ${message}</div>`,
}
const transporter = nodemailer.createTransport({
host: 'smtp.titan.email',
secure: true,
port: 465,
auth: auth,
})
const server = await new Promise((resolve, reject) => {
// verify connection configuration
transporter.verify(function (error: any, success: any) {
if (success) {
resolve(success)
}
reject(error)
})
})
if (!server) {
res.status(500).json({ error: 'Error failed' })
}
const success = await new Promise((resolve, reject) => {
// send mail
transporter.sendMail(mailData).then((info: any, err: any) => {
if (info.response.includes('250')) {
resolve(true)
}
reject(err)
})
})
if (!success) {
res.status(500).json({ error: 'Error sending email' })
}
res.status(200).json({ success: success })
}

Authenticate Firebase with Auth0 using Netlify Lambda Functions

I have a web app built with Gatsby that has client-side authentication through Auth0. I want to use Firebase as a database for my project, but I need to authenticate users first before they can read/write to Firebase.
The Firebase SDK (firebase-admin) has a function called signInWithCustomToken(token) that I thought I could pass the token from Auth0 into, but this doesn't work (see: https://community.auth0.com/t/react-auth0-firebase/11392).
Instead, I need to proxy Auth0's token through an API which will use firebase-admin to issue a token. Because my Gatsby site is hosted on Netlify, I'm planning to use Netlify Lambda Functions to get proxy Auth0's token. This is where I'm getting stuck.
I've followed this tutorial on how to use Netlify Lambda Functions with Gastsby: https://www.gatsbyjs.org/blog/2018-12-17-turning-the-static-dynamic/
I then went into my Auth.js file where my Auth0 code is and dropped a fetch call in the setSession. I passed the idToken from Auth0 into the url in the fetch function. I'm not sure if this is the right thing to do. I've read in the tutorial that it would be passed in an authorization header, but I'm unclear what that means. Anyways, here's the complete auth.js file:
import auth0 from 'auth0-js';
const windowGlobal = typeof window !== 'undefined' && window;
class Auth {
auth0 = new auth0.WebAuth({
domain: process.env.Auth_Domain,
clientID: process.env.Auth_ClientId,
redirectUri: process.env.Auth_Callback,
responseType: 'token id_token',
scope: 'openid profile email',
});
constructor() {
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
}
login() {
this.auth0.authorize();
}
logout() {
// Remove the locally cached profile to avoid confusing errors.
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
localStorage.removeItem('user');
windowGlobal.window.location.replace(`https://login.skillthrive.com/v2/logout/?returnTo=http%3A%2F%2Flocalhost:8000`)
}
handleAuthentication() {
if (typeof window !== 'undefined') {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult)
} else if (err) {
console.log(err);
}
});
}
}
isAuthenticated() {
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
setSession(authResult) {
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
fetch(`/.netlify/functions/firebase?id=${authResult.idToken}`)
.then(response => console.log(response))
this.auth0.client.userInfo(authResult.accessToken, (err, user) => {
localStorage.setItem('user', JSON.stringify(user));
})
}
getUser() {
if (localStorage.getItem('user')) {
return JSON.parse(localStorage.getItem('user'));
}
}
getUserName() {
if (this.getUser()) {
return this.getUser().name;
}
}
}
export default Auth;
I found a tutorial called How to Authenticate Firebase and Angular with Auth0 that has a function that mints a token for Firebase:
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
// Config
const config = require('./config');
module.exports = function(app) {
// Auth0 athentication middleware
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
audience: config.AUTH0_API_AUDIENCE,
issuer: `https://${config.AUTH0_DOMAIN}/`,
algorithm: 'RS256'
});
// Initialize Firebase Admin with service account
const serviceAccount = require(config.FIREBASE_KEY);
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: config.FIREBASE_DB
});
app.get('/auth/firebase', jwtCheck, (req, res) => {
// Create UID from authenticated Auth0 user
const uid = req.user.sub;
// Mint token using Firebase Admin SDK
firebaseAdmin.auth().createCustomToken(uid)
.then(customToken =>
// Response must be an object or Firebase errors
res.json({firebaseToken: customToken})
)
.catch(err =>
res.status(500).send({
message: 'Something went wrong acquiring a Firebase token.',
error: err
})
);
});
I tried to incorporate small parts at a time into my Lambda function:
var admin = require("firebase-admin");
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
// For more info, check https://www.netlify.com/docs/functions/#javascript-lambda-functions
export function handler(event, context, callback) {
console.log("queryStringParameters", event.queryStringParameters);
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.Auth_Domain}/.well-known/jwks.json`
}),
audience: process.env.Auth_Audience,
issuer: `https://${process.env.Auth_Domain}/`,
algorithm: 'RS256'
});
callback(null, {
// return null to show no errors
statusCode: 200, // http status code
body: JSON.stringify({
msg: "Hello, World! " + Math.round(Math.random() * 10),
}),
})
}
I tried checking to see what came back for jwtCheck by console logging it, but all I got was something weird { [Function: d] unless: [Function], UnauthorizedError: [Function: r] }
How should I go about incorporating this into my Lambda function?
I found a module called serverless-http that allows me to write Lambda Function as if it were written in Express. This made it easy for me to wrap my head around what was happening, so I finally got this code to return the new minted token from Firebase:
const express = require('express');
const serverless = require('serverless-http');
const cors = require('cors');
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
const app = express();
app.use(cors());
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${process.env.Auth_Domain}/.well-known/jwks.json`
}),
audience: `${process.env.Auth_ClientId}`,
issuer: `${process.env.Auth_Domain}`,
algorithm: 'RS256'
});
const serviceAccount = require('../firebase/firebase-keys.json');
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
});
// GET object containing Firebase custom token
app.get('/firebase', jwtCheck, async (req, res) => {
const {sub: uid} = req.user;
try {
const firebaseToken = await firebaseAdmin.auth().createCustomToken(uid);
res.json({firebaseToken});
} catch (err) {
res.status(500).send({
message: 'Something went wrong acquiring a Firebase token.',
error: err
});
}
});
module.exports.handler = serverless(app);
Then on the client side I wrapped the fetch call into a function like this and used it when needed:
async setFirebaseCustomToken() {
const response = await fetch('/.netlify/functions/firebase', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('id_token')}`,
},
});
const data = await response.json();
console.log(data.firebaseToken);
}
This code is just going to console.log the new token, but now you'll have the response to do what you want with in Firebase client-side. Hope this helps!

Resources