Storing Firebase accessToken in Pinia store - Vue3 - firebase

I'm fairly new to Vue and it's my first time using Pinia. I'm following this guide to set up Firebase, Pinia and Axios. The app I'm building uses FirebaseUI to sign a user in, via an email link - this all happens in the LoginPage component below:
(Please ignore all incorrectly types variables/functions - I'm just trying to get this working in the first place)
<script setup lang="ts">
import { onMounted } from "vue";
import { EmailAuthProvider } from "firebase/auth";
import { auth } from "firebaseui";
import { auth as firebaseAuth } from "../firebase/config";
import { useUserStore } from "../stores/user"
onMounted(async () => {
const uiConfig: auth.Config = {
signInSuccessUrl: "/",
signInOptions: [
{
provider: EmailAuthProvider.PROVIDER_ID,
signInMethod: EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD,
forceSameDevice: true,
},
],
callbacks: {
signInSuccessWithAuthResult: function (authResult) {
const store = useUserStore();
store.user = authResult;
return true;
},
},
};
const ui = new auth.AuthUI(firebaseAuth);
ui.start("#firebaseui-auth-container", uiConfig);
});
</script>
<template>
<div id="firebaseui-auth-container"></div>
</template>
When the user successfully signs in, the app updates the Pinia store user object, with the AuthResult return object from the signInSuccessWithAuthResult function. When debugger, I can see that the object being stored looks like the following:
{
additionalUserInfo: {...}
operationType: "signIn"
user: {
accessToken: "eyJhbGciOiJSUzI1N..."
auth: {...}
displayName: null
...
}
}
I.e. the accessToken is being stored. The user store is below:
import { defineStore } from 'pinia'
export const useUserStore = defineStore("userStore", {
state: () => ({
user: null as any
}),
getters: {
getUser(state) {
return state.user
}
}
})
In the app I have set up an axios interceptor, that appends the accessToken to any Axios request made by the app:
axiosInstance.interceptors.request.use((config) => {
const userStore = useUserStore();
if (userStore) {
debugger;
// accessToken is undefined
config.headers.Authorization = 'Bearer ' + userStore.user.user.accessToken;
}
return config;
});
When attempting the retrieve the accessToken from the user store at this point, it's gone. Most (if not all) of the other properties from the user object still exist, but not the access token, therefore I'm pretty sure I'm using the store correctly:
{
additionalUserInfo: {...}
credential: null
operationType: "signIn"
user: {
// accessToken is gone
apiKey: "..."
appName: "[DEFAULT]"
email: "..."
emailVerified: true
....
}
}
Can anybody explain where I'm going wrong with this, and why the accessToken is being removed from the store? It looks to me as though I'm using the Pinia store correctly, and I'm pretty sure that the interceptor is also correct. However it's likely that I'm going about storing the access token in the wrong way. I'd appreciate any help/advice about how to setup Firebase authentication correctly with Vue.
Edited to include value of the user store when debugging inside the interceptor.

It looks like accessToken might be in userStore.user.user.accessToken?

Im just finishing the same battle that you are in... IMO there are many ways that this setup can be configured... This is similar to why you might use callbacks in one place, and async await in another it depends on your project structure.
Heres a simple example that might help you clarify it.
first
create a firebase file to hold the config put this where ever your organization habits tells you to put it. Just remember so we can use it later.
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: "",
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
//initialize firebase auth
export const auth = getAuth(app);
Second - userStore
The user store does the legwork. We will use the actions when we want to interact with userauth from our ui.
import {
createUserWithEmailAndPassword,
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
import { auth } from "../firebase"; // the file we made above
import router from "../router";
export const useUserStore = defineStore("userStore", {
state: () => ({
userData: null,
loadingUser: false,
loadingSession: false,
}),
actions: {
async registerUser(email, password) {
this.loadingUser = true;
try {
const { user } = await createUserWithEmailAndPassword(
auth,
email,
password
);
this.userData = { email: user.email, uid: user.uid };
router.push("/");
} catch (error) {
console.log(error);
} finally {
this.loadingUser = false;
}
},
async loginUser(email, password) {
this.loadingUser = true;
try {
const { user } = await signInWithEmailAndPassword(
auth,
email,
password
);
this.userData = { email: user.email, uid: user.uid };
router.push("/");
} catch (error) {
console.log(error);
} finally {
this.loadingUser = false;
}
},
async logOutUser() {
try {
await signOut(auth);
this.userData = null;
router.push("/login");
} catch (error) {
console.log(error);
}
},
currentUser() {
return new Promise((resolve, reject) => {
const unsuscribe = onAuthStateChanged(
auth,
(user) => {
if (user) {
this.userData = { email: user.email, password: user.password };
} else {
this.userData = null;
}
resolve(user);
},
(e) => reject(e)
);
unsuscribe();
});
},
},
});
*** step3 setup the login / reg components in vue. ***
<div>
<form #submit.prevent="login">
<label>
Email:
<input type="email" v-model="email" required />
</label>
<br />
<label>
Password:
<input type="password" v-model="password" required />
</label>
<br />
<button type="submit">Login</button>
</form>
</div>
</template>
<script>
import { useUserStore } from "../stores/user";
export default {
data() {
return {
email: "",
password: "",
};
},
methods: {
async login() {
try {
await this.userStore.loginUser(this.email, this.password); //
} catch (error) {
console.error(error);
}
},
},
// because of below setup you can access this.userStore() singleton
setup() {
const userStore = useUserStore();
return {
userStore,
};
},
};
</script>
register is going to be simailar
<div>
<form #submit.prevent="register">
<label>
Email:
<input type="email" v-model="email" required />
</label>
<br />
<label>
Password:
<input type="password" v-model="password" required />
</label>
<br />
<button type="submit">Register</button>
</form>
</div>
</template>
<script>
import { useUserStore } from "../stores/user";
export default {
data() {
return {
email: "",
password: "",
};
},
methods: {
async register() {
try {
await this.userStore.registerUser(this.email, this.password);
} catch (error) {
console.error(error);
}
},
},
setup() {
const userStore = useUserStore();
return {
userStore,
};
},
};
</script>
now whenever you want to access the user it is in userStore.userData
if you dont have the userStore up yet just use the useUserStore() method and access it the same way you do from the setup in login / register view

Related

How to pass additional parameters in next-auth social login in NextJs?

I need to pass additional parameters to signIn function using next-auth in a NextJs project.
Here is what I tried.
<button
onClick={() =>
signIn(providers.facebook.id, { userType: "customer" })
}
>
<img src="images/facebook.svg" className="w-5 h-5" />
</button>
[...nextAuth].js code
import NextAuth from "next-auth";
import dbConnect from "../../../lib/dbConnect";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "../../../models/User";
import brcypt from "bcryptjs";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import InstagramProvider from "next-auth/providers/instagram";
dbConnect();
export default NextAuth({
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
//The providers are the authentication method
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: "Credentials",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
try {
const email = credentials.email;
const password = credentials.password;
const user = await User.findOne({ email: email });
if (!user) {
return null;
}
if (user) {
let allow = await signInUser({ password, user });
if (allow == true) {
return user;
} else {
return null;
}
}
} catch (error) {
return null;
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl:
"https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
authorization: {
params: {
userType: "customer" || "admin",
},
},
}),
InstagramProvider({
clientId: process.env.INSTAGRAM_CLIENT_ID,
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
}),
],
pages: {
signIn: "/login",
},
database: process.env.MONGODB_URI,
callbacks: {
async jwt(token, profile) {
console.log("jwt token>>>>", token);
console.log("jwt profile>>>>", profile);
return token;
},
async session({ session, user, token }) {
if (token) {
const name = token.token.user.name;
const email = token.token.user.email;
const image = token.token.user.image;
const platform = token.token.account.provider;
handleUser(name, email, image, platform);
}
return token.token.token;
},
},
});
const signInUser = async ({ password, user }) => {
let allow = true;
if (!password) {
allow = false;
}
const isMatch = await brcypt.compare(password, user.password);
if (!isMatch) {
allow = false;
}
return allow;
};
async function handleUser(name, email, image, platform) {
console.log("Handle User>>>>>", name);
console.log("Handle email>>>>>", email);
console.log("Handle image>>>>>", image);
console.log("Handle platform>>>>>", platform);
}
Inside the callbacks function I tried logging token & profile. The additional params I passed is not being sent.
What is the right way to achieve this in Next.js?
You can find an article on the GitHub discussion section for next-auth here. The general gist of the answer is that you specify additional custom parameters in the Google Provider parameter (as below). There are other steps, but overall they appear to have solved it. Hope this helps.
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
userType: "user" || "admin", <-- THIS ONE
},
},
}),

How can i test 'auth().signInWithCredential' in firebase/app?

I am writing test code using firebase/auth and google login with react-native
But, there are always bug in my test code
my UI component is below.
import { GoogleSignin } from "#react-native-google-signin/google-signin";
import auth from "#react-native-firebase/auth";
let userInfo;
try {
userInfo = await GoogleSignin.signIn();
} catch (error) {
console.error(error);
}
const googleCredential = auth.GoogleAuthProvider.credential(userInfo.idToken);
let testPromise;
try {
testPromise = await auth().signInWithCredential(googleCredential);
} catch (error) {
// In real code environment, there are no problem.
// But, In my test environment, the engine of the code always occur error in here
console.error(error);
}
The error message is below.
console.error
TypeError: (0 , _auth.default) is not a function
my mocking part of the test code is below
jest.mock("#react-native-firebase/auth", () => {
return {
GoogleAuthProvider: {
credential: jest.fn().mockReturnValue({ providerId: "fakeProviderId", secret: "fakeSecret", token: "fakeToken" }),
},
signInWithCredential: jest.fn(),
};
});
some tricks are in there.
i think the way i suggests is not best practice.
but, you can do this problem like this.
In react component...
import auth from "#react-native-firebase/auth";
import { firebase } from "#react-native-firebase/auth"; << KEY POINT
test component...
const mockedFbAuth = jest.fn();
jest.mock("#react-native-firebase/auth", () => {
return {
firebase: {
auth: jest.fn().mockImplementation(() => ({
signInWithCredential: mockedFbAuth.mockReturnValue({
additionalUserInfo: {
profile: {
email: "fakeCredentialEmail",
name: "fakeCredentialName",
},
},
}),
})),
},
};
});
above this way, i can solve my problem!

Firebase google authentication need to double-click to log in

I try to login into firebase with google authentication (using React hooks), and I need to click on the button twice. I want the user to get into the homepage after the first click. What is wrong here?
googleLogin.js
import firebase from "firebase/app";
import { auth,app } from "../../../../config/firebase";
export const googleLogin = async (e, information, setInformation, signup, login, history) => {
var provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
let token = credential.accessToken;
if (userInfo.isNewUser) signupWithGoogle(user, credential, userInfo);
})
.then(()=>{
history.push('/')
})
.catch((error) => {
...
});
};
const signupWithGoogle = (user, credential, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
return batch.commit();
}
I had the same problem to login and the problem was in my HTLM code.
I was using a form.
To resolve it, I used a simpler code in HTLM.

vue3 async setup() make my template not showing

I cannot figure out why my data is not showing when I use async front of setup().
My component call a module that handle my api calls. From that api I receive a token that I want to display in my client, as a test.
I can display the token i'm requesting in the console.log but can't see it in the template area.
Login.vue :
<template>
<h1>LOGIN</h1>
<Suspense>
<template #default>
<div>My token: {{ token }}</div>
</template>
<template #fallback>
<div>Loading</div>
</template>
</Suspense>
</template>
<script>
import useLogin from '../modules/users';
export default {
async setup() {
const { login, token } = useLogin();
await login();
return { token };
},
};
</script>
My module user.js
import api from '../services/Api';
import { ref } from 'vue';
export default function useLogin() {
const token = ref(null);
const login = async () => {
try {
token.value = await api.userLoginToken({
email: 'user#test.com',
password: '123123',
});
console.log(token.value);
} catch (error) {
console.log('error:', error);
return new Error();
}
};
return { login, token };
}
my api call is handled from api.js with axios:
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
export default {
async userLoginToken(credentials) {
const token = await apiClient.post('/users/login/token', credentials);
return token.data.token;
},
};
The whole Login.vue is blank
IMPORTANT: the data is showing if I remove async from of setup() and await front of login().
But I don't understand why i cannot use async.
Have to make the Suspense in the parent compoenent and move the fetch logic into a dedicated document to make it work.

Can't sign in with correct Email address and Password with Firebase

I create one web App for graduation research (developed with Vue.js, vue-router). I'm using Firebase Authentication to sign in. Even though using the correct Email Address and password, I can't sign in and the site redirect from 'localhost:8080/signin' to 'localhost:8080/signin?' .
This is developed with Vue(2.6.10) and firebase.
(ellipsis)
input(type="text" placeholder="your#email.com" v-model="email")#MailAddress
(ellipsis)
input(type="password" placeholder="password" v-model="password")#Password
(ellipsis)
import firebase from "firebase";
export default {
name: "Signin",
data() {
return {
email: "",
password: ""
};
},
methods: {
signIn() {
firebase
.auth()
.signInWithEmailAndPassword(this.email, this.password)
.then(
() => {
alert("Success");
this.$router.push("/");
},
err => {
alert(err.message);
}
);
}
}
};
I expect to redirect to 'localhost:8080/'
This works for me.
In my Vue component:
import firebase from '../database';
async signIn () {
let result = await firebase.signIn(this.email, this.password);
if (result.message) {
this.error = result.message;
} else {
// Go to your route
}
}
In my database file:
const database = firebase.initializeApp(config);
database.signIn = async (email, password) => {
try {
await firebase.auth().signInWithEmailAndPassword(email, password);
return true;
} catch (error) {
return error;
}
};

Resources