Firebase Cloud Messaging Web - onMessage not firing - javascript

I am trying to integrate FCM into my Next.js app. I have a hook to handle the messaging, but it isn't receiving my test messages. The code produces no errors either client side or server side, and logging shows that the onMessage callback is set, but after calling the API to send a test message, the client does not execute the callback.
_app.tsx
export default function MyApp({ Component, pageProps }: AppProps) {
initializeFirebase();
useMessaging();
return <Component {...pageProps} />;
}
hooks.ts
export function useMessaging() {
const [user] = useAuthState(getAuth());
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// VAPID_KEY is imported from another file
getToken(getMessaging(), { vapidKey: VAPID_KEY })
.then((token) => {
setToken(token);
onMessage(getMessaging(), (message) => {
// For testing, log that the message was received
console.log({ message });
});
})
}, []);
useEffect(() => {
if (user && token) {
// For testing, log the token
console.log({ token });
}
}, [user, token]);
}
/api/test.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const serviceAccount = JSON.parse(
decodeURI((process.env as MoodGraphEnv).FIREBASE_SERVICE_ACCOUNT)
) as ServiceAccount;
if (getApps().length === 0) {
initializeApp({
credential: cert(serviceAccount),
databaseURL: 'https://my-app-123456-default-rtdb.firebaseio.com',
});
}
const message: Message = {
notification: {
title: 'Hello world!',
body: 'The notification worked!',
},
token:
// During testing I replace this with the token logged to the browser console
'token_goes_here',
};
getMessaging()
.send(message)
.then((messagingResponse) => {
console.log('Successfully sent message:', messagingResponse);
})
.catch((error) => {
console.log('Error sending message:', error);
});
res.status(200).json({});
}
I am not sure why the callback is not executed, so any help would be appreciated.

Related

React GraphQL authentication flow with userContext

I have a React application which allows the user to login via a form then navigates to the home page. I need the App component to provide the user down to its children.
App.js
function App() {
const [token, setToken] = useState(null)
const userQuery = useQuery(USER) // Gets user from GraphQL, using token in the request context
...
useEffect(() => {
setToken(localStorage.getItem("user-token"))
}, [])
return (
<UserContext.Provider value={userQuery.data}>
...
</UserContext.Provider>
)
}
LoginForm.js
const LoginForm = () => {
...
const [login, result] = useMutation(LOGIN, {
onError: (error) => {
console.log("error :>> ", error)
setError(error.graphQLErrors[0].message)
},
})
useEffect(() => {
if (result.data) {
const token = result.data.login.value
localStorage.setItem("user-token", token)
navigate("/")
}
}, [result.data])
...
}
index.js
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("user-token")
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
When the login form submits the GraphQL login query is executed. Then when the result is received the token is set in local storage and it navigates back to the main page. The problem is that the app's user query receives a null user because the request wasn't sent with the new storage token. How can I get it to do this without refreshing the page?

Where to set Sentry's setUser in Next.js app?

I have been trying to set user data into Sentry's scope globally, so every time there's an error or event, user info is passed to it.
My app is built in Next.js, so naturally I added the config as it is in Sentry's documentation for Next.js.
I haven't got the idea on where to add the Sentry.setUser({id: user.Id}) method in order for it to set the user globally.
So far I have added it to the Sentry's _error.js file, inside the getInitialProps method:
import NextErrorComponent from 'next/error';
import * as Sentry from '#sentry/nextjs';
import { getUser } from '../lib/session';
const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
Sentry.captureException(err);
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async (context) => {
const errorInitialProps = await NextErrorComponent.getInitialProps(context);
const { req, res, err, asPath } = context;
errorInitialProps.hasGetInitialPropsRun = true;
const user = await getUser(req, res);
// Set user information
if (user) {
console.log('Setting user');
Sentry.setUser({ id: user.Id });
}
else {
console.log('Removing user');
Sentry.configureScope(scope => scope.setUser(null));
}
if (res?.statusCode === 404) {
return errorInitialProps;
}
if (err) {
Sentry.captureException(err);
await Sentry.flush(2000);
return errorInitialProps;
}
Sentry.captureException(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
);
await Sentry.flush(2000);
return errorInitialProps;
};
export default MyError;
But when trying to log errors, the user info doesn't show in Sentry, only the default user ip:
I have also tried setting the user after successful login, and still nothing..
Help is appreciated!!
Not sure if this is the right way, but the above solutions didn't work for me. So I tried calling setUser inside _app.tsx.
import { useEffect } from "react";
import { setUser } from "#sentry/nextjs";
import { UserProvider, useUser } from "#auth0/nextjs-auth0";
import type { AppProps } from "next/app";
function SentryUserManager() {
const { user } = useUser();
useEffect(() => {
if (user) {
setUser({
email: user.email ?? undefined,
username: user.name ?? undefined,
});
} else {
setUser(null);
}
}, [user]);
return null;
}
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<UserProvider>
<Component {...pageProps} />
<SentryUserManager />
</UserProvider>
);
}
Still not sure why this worked for me and the other solutions didn't, but figured it was worth sharing.
I would suggest using the callback handler to set your Sentry user context.
import { handleAuth, handleLogin, handleCallback } from "#auth0/nextjs-auth0";
import * as Sentry from "#sentry/nextjs";
import { NextApiHandler } from "next";
const afterCallback = (_req, _res, session, _state) => {
Sentry.setUser({
id: session.user.sub,
email: session.user.email,
username: session.user.nickname,
name: session.user.name,
avatar: session.user.picture,
});
return session;
};
const handler: NextApiHandler = handleAuth({
async login(req, res) {
await handleLogin(req, res, {
returnTo: "/dashboard",
});
},
async callback(req, res) {
try {
await handleCallback(req, res, { afterCallback });
} catch (error) {
res.status(error.status || 500).end(error.message);
}
},
});
export default Sentry.withSentry(handler);
You can set the user in Sentry right after successful login
const handleLogin = {
try {
const res = await axios.post("/login", {"john#example.com", "password"})
if (res && res?.data) {
// Do other stuff
Sentry.setUser({ email: "john#example.com" });
}
}
}
Additionaly you can clear the user while logging out
const handleLogout = {
// Do othe stuff
Sentry.configureScope(scope => scope.setUser(null));
}

Nextjs auth with Firebase not sharing cookie accross pages

I'm trying to create auth using Firebase and NextJS
I followed this tutorial: [https://colinhacks.com/essays/nextjs-firebase-authentication][1].
Whenever I access an SSR authenticated page the token is deleted from the front end, firebase gives an error stating FirebaseAuthError: First argument to verifyIdToken() must be a Firebase ID token string..
It seems like the cookie is deleted whenever SSR is run because Firebase see's the cookie as invalid. I've tried setting the cookie using js-cookie and using nookies.
I've tried different cookie frameworks for setting cookies, the NextJS native method of fetching cookies SSR and nookies for fetching cookies SSR.
auth.js
const AuthContext = createContext<{ user: firebaseClient.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebaseClient.User | null>(null);
console.log("User is ", user)
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).nookies = nookies;
}
return firebaseClient.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
// Cookies.set("token", "");
nookies.set(null, "token", "",);
return;
}
const token = await user.getIdToken();
// Cookies.set("token", token);
nookies.set(null, "token", token, {
path: "/",
encode: (v) => v,
});
setUser(user);
});
}, []);
useEffect(() => {
const handle = setInterval(async () => {
console.log(`refreshing token...`);
const user = firebaseClient.auth().currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
export const useAuth = () => {
return useContext(AuthContext);
};
Authenticated page
export const getServerSideProps = async (ctx, req) => {
try {
const cookies = nookies.get(ctx);
// Fails here, never accepts token
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
console.log("Token is:", token)
return {
props: {
data: "Worked!"
},
};
} catch (err) {
return {
props: {
data: "Firebase failed!"
},
};
}
};
const Revisions = ({ data, ctx }) => {
const { user } = useAuth();
return (
<Layout>
<Container>
{/* always returns no user signed in */}
<p>{`User ID: ${user ? user.uid : 'no user signed in'}`}</p>
<p>{data}</p>
</Container>
</Layout>
);
};
EDIT: Additional information
On the loginpage cookie is set on the front end. I can see this cookie in my Applications tab.
[![Application tab chrome showing token and value][2]][2]
I have verified that localhost with the correct port is permitted through the firebase console.
The token seems to get deleted at this line
const token = await firebaseAdmin.auth().verifyIdToken(cookie);
I have tried using JSON parse, both with JSON.stringify first and just straight JSON parsing the data.
[1]: https://colinhacks.com/essays/nextjs-firebase-authentication
[2]: https://i.stack.imgur.com/p8MES.png

firebase reauthentication flow with Token in react js app not working properly

after an hour the user get disconnected from the firebase functions and gets an error.
the connection to firebase in the app work like this:
After the first connection via google, the token is sent to firebase functions.
after that a getIdToken(ture) to force a refresh in useEffect.
the token is saved in the state via mobX and every time a commend requires to send or get data from the data base it's passes the token to the firebase functions
I have noticed that I don't get a new token in .then(function (idToken) {...}
this is the error :
FirebaseAuthError: Firebase ID token has expired.
Get a fresh ID token from your client app and try again (auth/id-token-expired).
See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
...
...
...
> errorInfo: {
> code: 'auth/id-token-expired',
> message: 'Firebase ID token has expired.
Get a fresh ID token from your client app and try again (auth/id-token-expired).
See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.'
> },
> codePrefix: 'auth'
> }
Things that I have tried already:
is to separate the firebase.auth().currentUser.getIdToken(true).then() to a different useEffect().
call getIdToken() after I get an error from the firebase functions.
const UserSubscriber = () => {
//using mobX here
const { user } = useStores();
const token = user.store.token;
React.useEffect(() => {
if (!token.length || !firebase.auth().currentUser) return;
firebase.auth().currentUser.getIdToken(true).then(function (idToken) {
const decodedToken = jwt.decode(idToken, '', true);
if (!decodedToken.user_id) return;
const unsub = firebase.firestore().collection('users').doc(decodedToken.user_id).onSnapshot(docSnapshot => {
const data = docSnapshot.data();
//user.mergeData() is just to store data
if (!data) return user.mergeData({ noUser: true, token: idToken })
user.mergeData({ ...data, noUser: false, token: idToken })
});
return () => unsub();
}).catch(function (error) {
user.logOut();
});
}, [token, user]);
return useObserver(() => (
<div />
));
}
and in the backend
app.use(async (req, res, next) => {
try {
const decodedToken = await admin.auth().verifyIdToken(req.body.token);
let uid = decodedToken.uid;
req.uid = uid;
return next();
} catch (error) {
console.log(error);
return res.status(401).send();
}
});
I have tried firebase.auth().onAuthStateChanged(userAuth => {...}) (in side of the useEffect())
firebase.auth().onAuthStateChanged(userAuth => {
userAuth.getIdToken().then(function (idToken) {
const decodedToken = jwt.decode(idToken, '', true);
if (!decodedToken.user_id) return;
const unsub = firebase.firestore().collection('users').doc(decodedToken.user_id).onSnapshot(docSnapshot => {
const data = docSnapshot.data();
if (!data) return user.mergeData({ noUser: true, token: idToken })
user.mergeData({ ...data, noUser: false, token: idToken })
});
return () => unsub();
}).catch(function (error) {
user.logOut();
});
})
;
}

NuxtServerInit sets Vuex auth state after reload

I'm setting a basic authentication on a Nuxt project with JWT token and cookies to be parsed by nuxtServerInit function.
On login with email/password, works as intended, setUser mutation is triggered and the appropriate user object is stored in state.auth.user.
On reload, nuxtServerInit will get the jwt token from req.headers.cookies, call the GET method and identify user.Works like a charm.
Problem starts when I hit the /logout endpoint. state.auth.user is set to false and Im effectively logged out... but If I refresh, I'm logged in again with the previous user data. Even if my cookies are properly empty (on below code, both user and cookie are undefined after logout and refresh, as expected)
So I really don't get why is my state.auth.user is back to its initial value...
store/index.js
import Vuex from "vuex";
import auth from "./modules/auth";
import axios from "~/plugins/axios";
const cookieparser = process.server ? require("cookieparser") : undefined;
const END_POINT = "api/users";
const createStore = () => {
return new Vuex.Store({
actions: {
async nuxtServerInit({ commit, dispatch}, { req }) {
let cookie = null;
console.log(req.headers.cookie)
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie);
try {
cookie = JSON.parse(parsed.auth);
console.log("cookie", cookie)
const {accessToken} = cookie
const config = {
headers: {
Authorization: `Bearer ${accessToken}`
}
}
const response = await axios.get(`${END_POINT}/current`, config)
const user = response.data
console.log("user nuxt server init", user)
await commit('setUser', user)
} catch (err) {
// No valid cookie found
console.log(err);
}
}
}
},
modules: {
auth
}
});
};
export default createStore;
modules/auth.js
import axios from "~/plugins/axios";
const Cookie = process.client ? require("js-cookie") : undefined;
const END_POINT = "api/users";
export default {
state: {
user: null,
errors: {}
},
getters: {
isAuth: state => !!state.user
},
actions: {
login({ commit }, payload) {
axios
.post(`${END_POINT}/login`, payload)
.then(({ data }) => {
const { user, accessToken } = data;
const auth = { accessToken };
Cookie.set("auth", auth);
commit("setUser", user);
})
.catch(e => {
const error = e;
console.log(e);
commit("setError", error);
});
},
logout({ commit }) {
axios
.post(`${END_POINT}/logout`)
.then(({ data }) => {
Cookie.remove("auth");
commit("setUser", false);
})
.catch(e => console.log(e));
},
},
mutations: {
setUser(state, user) {
state.user = user;
},
setError(state, errors) {
state.errors = errors;
}
}
};
The way I logout my user is by creating a mutation called clearToken and commit to it in the action :
State :
token: null,
Mutations :
clearToken(state) {
state.token = null
},
Actions :
logout(context) {
context.commit('clearToken')
Cookie.remove('token')
}
This way, you token state revert back to null.

Categories

Resources