I have a screens where I am trying my hands on firebase's facebook authentication.
Using expo i can successfully generate facebook token. next what i did was to use this token to genrate cerdentials, which i think also ran successfully but when I am trying to use the credentials to signin to firebase i get error login failed.
I am not sure what is the problem. Do I need to register user using email & password before letting them login using facebook auth.
Any help will be appreciated...
here is the code...
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
Button,
StatusBar,
StyleSheet,
View,
Text,
} from 'react-native';
import Expo, { Facebook } from 'expo';
import * as firebase from 'firebase';
import ModalActivityIndicator from '../../components/ModalActivityIndicator';
export default class SignInFacebookScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
};
}
componentDidMount = async () => {
await this.facebookLogin();
};
facebookLogin = async () => {
const { type, token } = await Expo.Facebook.logInWithReadPermissionsAsync(
'<AppId>',
{
permissions: ['public_profile', 'email'],
}
);
if (type === 'success') {
await this.callGraph(token);
} else if (type === 'cancel') {
alert('Cancelled!', 'Login was cancelled!');
} else {
alert('Oops!', 'Login failed!');
}
};
callGraph = async token => {
const response = await fetch(
`https://graph.facebook.com/me?access_token=${token}`
);
const userProfile = JSON.stringify(await response.json());
const credential = firebase.auth.FacebookAuthProvider.credential(token);
await this.firebaseLogin(credential);
};
// Sign in with credential from the Facebook user.
firebaseLogin = async credential => {
firebase
.auth()
.signInAndRetrieveDataWithCredential(credential)
.then(() => {
this.setState({
isLoading: false,
hasError: false,
errorMessage: null,
});
this.props.navigation.navigate('App');
})
.catch(error => {
this.setState({
isLoading: false,
hasError: true,
errorMessage: error.errorMessage,
});
});
};
render() {
let { isLoading, hasError, errorMessage } = this.state;
return (
<View style={styles.container}>
<ModalActivityIndicator isLoading={!!isLoading} />
<Text>Sign In with Facebook</Text>
{hasError && (
<React.Fragment>
<Text style={[styles.errorMessage, { color: 'black' }]}>
Error logging in. Please try again.
</Text>
<Text style={[styles.errorMessage, { color: 'black' }]}>
{errorMessage}
</Text>
</React.Fragment>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Well I fixed the issue with the following code. Seemed like the issue was with channing of async functions.
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
Button,
StatusBar,
StyleSheet,
View,
Text,
} from 'react-native';
import Expo, { Facebook } from 'expo';
import * as firebase from 'firebase';
import ModalActivityIndicator from '../../components/ModalActivityIndicator';
export default class SignInFacebookScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
firstName: '',
lastName: '',
};
}
componentDidMount = async () => {
const fbToken = await this.getFacebookToken();
const userProfile = await this.getFacebookUserProfile(fbToken);
this.setUserDetails(userProfile);
const credential = await this.getFirebaseFacebookCredential(fbToken);
await this.loginToFirebaseWithFacebook(credential);
};
getFacebookToken = async () => {
const { type, token } = await Expo.Facebook.logInWithReadPermissionsAsync(
'<AppId>',
{
permissions: ['public_profile', 'email'],
}
);
if (type === 'success') {
return token;
} else if (type === 'cancel') {
alert('Cancelled!', 'Login was cancelled!');
} else {
alert('Oops!', 'Login failed!');
}
};
getFacebookUserProfile = async token => {
this.setState({ isLoading: true });
const response = await fetch(
`https://graph.facebook.com/me?access_token=${token}&fields=first_name,last_name`
);
const userProfile = JSON.stringify(await response.json());
return userProfile;
};
setUserDetails = userProfile => {
const userProfileObj = JSON.parse(userProfile);
this.setState({
firstName: userProfileObj.first_name,
lastName: userProfileObj.last_name,
});
};
getFirebaseFacebookCredential = async token => {
const credential = firebase.auth.FacebookAuthProvider.credential(token);
return credential;
};
loginToFirebaseWithFacebook = async credential => {
firebase
.auth()
.signInAndRetrieveDataWithCredential(credential)
.then(() => {
let user = firebase.auth().currentUser;
firebase
.database()
.ref('users/' + user.uid)
.update({
firstName: this.state.firstName,
lastName: this.state.lastName,
});
})
.then(() => {
this.setState({
isLoading: false,
haserror: false,
errorMessage: null,
});
this.props.navigation.navigate('App');
});
};
render() {
let { isLoading, hasError, errorMessage } = this.state;
return (
<View style={styles.container}>
<ModalActivityIndicator isLoading={!!isLoading} />
<Text>Sign In with Facebook</Text>
{hasError && (
<React.Fragment>
<Text style={[styles.errorMessage, { color: 'black' }]}>
Error logging in. Please try again.
</Text>
<Text style={[styles.errorMessage, { color: 'black' }]}>
{errorMessage}
</Text>
</React.Fragment>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Related
i need to secure the API so that only authorized user can access them. I followed the documentation in this link https://next-auth.js.org/tutorials/securing-pages-and-api-routes#securing-api-routes but apparently I am not retrieving the session.
I am able to console.log the authOptions but if I try to console log the session (and I am logged in), it logs "null"
This is the code
pages/api/profile.js
import prisma from "../../../lib/prisma";
import { unstable_getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req, res) {
const session = await unstable_getServerSession(req, res, authOptions);
console.log("SESSION", session); // this logs "null"
if (!session) {
return res.status(401).json("Not authorized");
}
try {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
return res.status(200).json(user);
} catch (error) {
console.error(error);
return res
.status(503)
.json(
"Our server is not able to process the request at the moment, please try again later!"
);
}
pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import CognitoProvider from "next-auth/providers/cognito";
import prisma from "../../../lib/prisma";
export const authOptions = {
providers: [
CognitoProvider({
clientId: process.env.CLIENTID_NEXTAUTH,
issuer: process.env.COGNITO_ISSUER,
clientSecret: process.env.CLIENTSECRET_NEXTAUTH,
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60,
updateAge: 24 * 60 * 60,
},
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token }) {
const user = await prisma.user.findUnique({
where: { email: session?.user?.email },
});
if (!user) throw new Error("User not found in the database.");
const mySession = {
...session,
accessToken: token.accessToken,
email: user.email,
};
return mySession;
},
},
};
export default NextAuth(authOptions);
pages/dashboard/index.js
import axios from "axios";
import React, { useState } from "react";
import { getSession, useSession } from "next-auth/react";
const Dashboard = (props) => {
let { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (status === "unauthenticated") {
window.location.reload();
}
return (
<p>
{props.userInfo.name}
</p>
);
};
export default Dashboard;
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
console.log("SESSION IN INDEX", session); // this logs the session
const userInfo = await axios.get(
`${process.env.BASE_URL}/api/profile?email=${session.email}`
);
return {
props: {
session,
userInfo: userInfo.data ? userInfo.data : null,
},
};
}
so when I login, I can see the SESSION in INDEX but when I hit the api/profile, the session from unstable_getServerSession is null, so I canno see nothing in the dashboard
resolved:
when calling the api you need to pass the headers, for example in the dashboard/index.js
const userInfo = await axios.get(
`${process.env.BASE_URL}/api/profiles/profile?email=${session.email}`,
{
withCredentials: true,
headers: {
Cookie: context.req.headers.cookie,
},
}
);
while in the API endpoint
import { getServerSession, getSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions);
console.log("SESSION", session);
//your code
}
The following works fine, but I have noticed that it is really slow login in a client. How can I make it faster?
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { ObjectId } from 'mongodb'
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import clientPromise from "../../../lib/mongodb";
import { v4 as uuidv4 } from 'uuid';
var CryptoJS = require("crypto-js");
const sFinder = async (task, token) => {
try{
const client = await clientPromise;
const database = client.db('DRN1');
const ses = await database.collection('sessions');
switch (task) {
case 1:
const result = await ses.find({
"userId": ObjectId(token.uuid)
}).sort({"_id":-1}).limit(1).toArray();
if (!result) {
return 202;
}
else{
return result[0].sessionToken
}
break;
case 2:
const insertResult = await ses.insertOne({"userId":token.uuid, "sessionToken":token.accessToken});
if (!insertResult) {
return 203;
}
else{
return insertResult
}
break;
case 3:
var expdate = new Date(token.exp * 1000);
const UpdateResult = await ses.updateOne({"userId":ObjectId(token.uuid), "sessionToken":token.accessToken},
{ $set: {"expires": expdate}}, { upsert: true });
if (!UpdateResult) {
return 203;
}
else{
return UpdateResult
}
break;
default:
break;
}
} catch(e){
console.error(e);
}
}
export default NextAuth({
adapter: MongoDBAdapter(clientPromise),
session: {
strategy: 'jwt',
jwt: true,
},
providers: [
CredentialsProvider({
name: 'DRN1',
credentials: {
username: { label: "Username", type: "text"},
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
try{
const client = await clientPromise;
const database = client.db('DRN1');
const users = await database.collection('users');
const result = await users.findOne({
username: credentials.username,
});
if (!result) {
throw new Error('No user found with the username');
}
var bytes = CryptoJS.AES.decrypt(result.password, process.env.PASS_ENC);
var decryptedData = bytes.toString(CryptoJS.enc.Utf8);
//Check hased password with DB password
if(decryptedData != credentials.password){
throw new Error('Password doesnt match');
}
return {uuid:result._id, username: result.username, email: result.email, type:result.type, "sessionID":uuidv4()};
} catch(e){
console.error(e)
}
}
})
],
callbacks: {
signIn: async ({ user, account, profile, email, credentials }) => {
account.accessToken = user.sessionID
account.uuid = user.uuid
const test = await sFinder(2,account)
return true
},
jwt: async ({ token, account }) => {
if (account) {
token.uuid = account.uuid
token.accessToken = account.accessToken
}
const lastUsedToken = await sFinder(1,token)
const updateTokenExpire = await sFinder(3,token)
if(lastUsedToken != token.accessToken){
// console.log("I have made it an error")
token.error = 555;
}
return token
},
session: async ({ session, token, user }) => {
session.uuid = token.uuid
if(!token.accessToken){
//OAUTH Accounts
session.accessToken = uuidv4()
}else{
session.accessToken = token.accessToken
}
if(token.error == 555){
session.error = 555
}
return session
}
},
pages:{
error: 'signin'
},
theme: {
colorScheme: "dark", // "auto" | "dark" | "light"
brandColor: "", // Hex color code
logo: "https://storage.googleapis.com/radiomedia-images/station_logos/v2/DRN1_small.png" // Absolute URL to image
}
});
I believe what is slowing it down is the following
callbacks: {
signIn: async ({ user, account, profile, email, credentials }) => {
account.accessToken = user.sessionID
account.uuid = user.uuid
const test = await sFinder(2,account)
return true
},
jwt: async ({ token, account }) => {
if (account) {
token.uuid = account.uuid
token.accessToken = account.accessToken
}
const lastUsedToken = await sFinder(1,token)
const updateTokenExpire = await sFinder(3,token)
if(lastUsedToken != token.accessToken){
// console.log("I have made it an error")
token.error = 555;
}
return token
},
session: async ({ session, token, user }) => {
session.uuid = token.uuid
if(!token.accessToken){
//OAUTH Accounts
session.accessToken = uuidv4()
}else{
session.accessToken = token.accessToken
}
if(token.error == 555){
session.error = 555
}
return session
}
},
Mainly all the awaits, but the await functions are to make sure the user is not login on another device. As we log the old devices out automatically.
My problem is that initialState from slice.js not changing, when I console.log store(using UseSelector) I see that state.list empty and did not changed.I'm trying to catch data from GET endpoint, endpoint is working.
store.js
import { configureStore } from '#reduxjs/toolkit';
import shopReducer from '../connection/shopSlice';
export const store = configureStore({
reducer: {
shop: shopReducer,
},
devTools: true
});
slice.js
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import axios from 'axios';
export const getProducts = createAsyncThunk(
'shop/getProducts',
async () => {
const response = await axios.get('http://localhost:3000/products');
return response.data;
}
);
export const listOfProducts = createSlice({
name: 'shop',
initialState: {
list: [],
status: 'idle',
error: null,
},
reducers: {
addProduct: {
reducer: (state, action) => {
state.list.push(action.payload);
},
prepare(value) {
return {
payload: {
key: value.id,
value: value,
},
};
},
},
},
extraReducers: {
[getProducts.pending]: (state, action) => {
state.status = 'loading';
},
[getProducts.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.list.push(...action.payload);
},
[getProducts.rejected]: (state, action) => {
state.status = 'failed';
state.error = action.error.message;
},
},
});
export const { addProduct } = listOfProducts.actions;
export default listOfProducts.reducer;
component with console.log
import React from 'react';
import common from './common.module.scss';
import shopCards from './shopCards.module.scss';
import { useSelector } from "react-redux";
const ShopCards = () => {
console.log(useSelector(state=>state))
return (
<div>
</div>
);
};
export default ShopCards;
The issue is that you are not dispatching the getProducts at all, you should dispatch this action and get the state with useSelector(state => state.shop) to select the proper reducer state. Try to change your code to the following:
import React from 'react';
import common from './common.module.scss';
import shopCards from './shopCards.module.scss';
// Don't forget to change the path
import { getProducts } from './path/to/reducer'
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
const ShopCards = () => {
const products = useSelector((state) => {state.shop});
useEffect(() => {
// dispatch the action on first render
useDispatch(getProducts());
}, []);
useEffect(() => {
// print the products if the fetch to backend was made successfully
console.log(products);
}, [products]);
return (
<div>
</div>
);
};
export default ShopCards;
Other thing, in your createAsyncThunk you are returning response.data, so to fulfill properly the state, your api response should looks like this:
{
// you must return a JSON with data key who has an array of your products
// the content of product was just an example, so ignore it
data: [{id: 1, product: "foo"}]
}
i use redux toolkit with react native and mongodb (mongoose)
i delete item and it successfully deleted from db
but not in client and need to reload page
todoSlice :
import {createSlice} from '#reduxjs/toolkit';
export const todoSlice = createSlice({
name: 'todos',
initialState: {
todos: [],
pending: null,
error: null,
},
reducers: {
deleteTodo: (state, action) => {
return state
},
},
});
export const {deleteTodo} = todoSlice.actions;
export default todoSlice.reducer;
apiCall:
import axios from 'axios';
import {deleteTodo} from './todoSlice';
export const deleteOneTodo = async (id, dispatch) => {
try {
await axios.delete(`http://10.0.2.2:5000/todos/${id}`);
dispatch(deleteTodo());
} catch (err) {
console.log(err);
}
};
main :
const {todo} = useSelector(state => state);
const dispatch = useDispatch();
const {todos} = todo;
useEffect(() => {
getTodos(dispatch);
}, []);
const handleDelete = id => {
deleteOneTodo(id, dispatch);
};
you have to implement deleteTodo inside your todoSlice in order to remove the deleted id from your local state,
...
export const todoSlice = createSlice({
name: 'todos',
initialState: {
todos: [],
pending: null,
error: null,
},
reducers: {
deleteTodo: (state, action) => {
return state.filter((todo)=>todo.id!==action.payload.id);
},
},
});
...
and of course you have to pass the payload with the id of the todo you want to remove
export const deleteOneTodo = async (id, dispatch) => {
try {
await axios.delete(`http://10.0.2.2:5000/todos/${id}`);
dispatch(deleteTodo({id:id}));
} catch (err) {
console.log(err);
}
};
if you still have doubts you can follow this tutorial: https://www.youtube.com/watch?v=fiesH6WU63I
i just call 'getTodos' inside 'deleteOneTodo'
and delete 'deleteTodo' from reducer
i hope its a good practice
export const deleteOneTodo = async (id, dispatch) => {
try {
await axios.delete(`http://10.0.2.2:5000/todos/${id}`);
// i add this line =>
getTodos(dispatch);
} catch (err) {
console.log(err);
}
};
I am following this tutorial using Express.js Postgres and Vue to create a CRUD app for Songs with login and basic associations : https://www.youtube.com/watch?v=ipYlztBRpp0
I am using Vuex to manage the state object. However, for some reason, though registration of new users works to create a user record, when I try to create a Bookmark, which essentially pairs a UserId with a SongId, UserId is always recorded as null. SongId is saved correctly. Checking the requests in the network and Vue tabs of developer tools reveals that the User object seems to be empty.
Here are the relevant files.
User.js model
const Promise = require('bluebird')
const bcrypt = Promise.promisifyAll(require('bcrypt-nodejs'))
function hashPassword (user, options) {
const SALT_FACTOR = 8
if (!user.changed('password')) {
return;
}
return bcrypt
.genSaltAsync(SALT_FACTOR)
.then(salt => bcrypt.hashAsync(user.password, salt, null))
.then(hash => {
user.setDataValue('password', hash)
})
}
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
unique: true
},
password: DataTypes.STRING
}, {
hooks: {
// beforeCreate: hashPassword,
// beforeUpdate: hashPassword,
beforeSave: hashPassword
}
})
User.prototype.comparePassword = function (password) {
return bcrypt.compareAsync(password, this.password)
}
return User
}
Songs model:
module.exports = (sequelize, DataTypes) => {
const Song = sequelize.define('Song', {
title: DataTypes.STRING,
artist: DataTypes.STRING,
genre: DataTypes.STRING,
album: DataTypes.STRING,
albumImageUrl: DataTypes.STRING,
youtubeId: DataTypes.STRING,
lyrics: DataTypes.TEXT,
tab: DataTypes.TEXT
})
return Song
}
Bookmark's controller:
const {Bookmark} = require('../models')
module.exports = {
async index (req, res) {
try {
const {songId, userId} = req.query
const bookmark = await Bookmark.findOne({
where: {
SongId: songId,
UserId: userId
}
})
res.send(bookmark)
} catch (err) {
res.status(500).send({
error: 'An error has occurred trying to fetch the songs'
})
}
},
async post (req, res) {
try {
const {songId, userId} = req.body
const bookmark = await Bookmark.findOne({
where: {
SongId: songId,
UserId: userId
}
})
if (bookmark) {
return res.status(400).send({
error: 'you already have this song bookmarked'
})
}
const newBookmark = await Bookmark.create({
SongId: songId,
UserId: userId
})
res.send(newBookmark)
} catch (err) {
console.log(err)
res.status(500).send({
error: 'An error has occurred trying to create the bookmark.'
})
}
},
async delete (req, res) {
try {
const {bookmarkId} = req.params
const bookmark = await Bookmark.findById(bookmarkId)
await bookmark.destroy()
res.send(bookmark)
} catch (err) {
res.status(500).send({
error: 'An error has occurred trying to delete the bookmark'
})
}
}
}
BookMarksService (for axios connection)
import Api from '#/services/Api'
export default {
index (bookmark) {
return Api().get('bookmarks', {
params: bookmark
})
},
post (bookmark) {
return Api().post('bookmarks', bookmark)
},
delete (bookmarkId) {
return Api().delete(`bookmarks/${bookmarkId}`)
}
}
and the component SongsMetaData:
<template>
<panel title='Song Metadata'>
<v-layout>
<v-flex xs6>
<div class="song-title">
{{song.title}}
</div>
<div class="song-artist">
{{song.artist}}
</div>
<div class="song-genre">
{{song.genre}}
</div>
<v-btn
dark
class="cyan"
:to="{
name: 'song-edit',
params () {
return {
songId: song.id
}
}
}">
Edit
</v-btn>
<v-btn
v-if="isUserLoggedIn && !bookmark"
dark
class="cyan"
#click="setAsBookmark">
Bookmark
</v-btn>
<v-btn
v-if="isUserLoggedIn && bookmark"
dark
class="cyan"
#click="unsetAsBookmark">
UnBookmark
</v-btn>
</v-flex>
<v-flex xs6>
<img class="album-image" :src="song.albumImageUrl" />
<br />
<div class="song-album">
{{song.album}}
</div>
</v-flex>
</v-layout>
</panel>
</template>
<script>
import {mapState} from 'vuex'
import BookmarksService from '#/services/BookmarksService'
export default {
props: [
'song'
],
data () {
return {
bookmark: null
}
},
computed: {
...mapState([
'isUserLoggedIn'
])
},
watch: {
async song () {
if (!this.isUserLoggedIn) {
return
}
try {
this.bookmark = (await BookmarksService.index({
songId: this.song.id,
userId: this.$store.state.user.id
})).data
} catch (err) {
console.log(err)
}
}
},
methods: {
async setAsBookmark () {
try {
this.bookmark = (await BookmarksService.post({
songId: this.song.id,
userId: this.$store.state.user.id
})).data
} catch (err) {
console.log(err)
}
},
async unsetAsBookmark () {
try {
await BookmarksService.delete(this.bookmark.id)
} catch (err) {
console.log(err)
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.song {
padding: 20px;
height: 330px;
overflow: hidden;
}
.song-title {
font-size: 30px;
}
.song-artist {
font-size: 24px;
}
.song-genre {
font-size: 18px;
}
.album-image {
width: 70%;
margin: 0 auto;
}
textarea {
width: 100%;
font-family: monospace;
border: none;
height: 600px;
border-style: none;
border-color: transparent;
overflow: auto;
padding: 40px;
}
</style>
and the vuex store.js file:
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export default new Vuex.Store({
strict: true,
plugins: [
createPersistedState()
],
state: {
token: null,
user: null,
isUserLoggedIn: false
},
mutations: {
setToken (state, token) {
state.token = token
if (token) {
state.isUserLoggedIn = true
} else {
state.isUserLoggedIn = false
}
},
setUser (state, user) {
state.user = user
}
},
actions: {
setToken ({commit}, token) {
commit('setToken', token)
},
setUser ({commit}, user) {
commit('setUser', user)
}
}
})