Published: October 06 2020

Vue 3 - Facebook Login Tutorial & Example

Tutorial built with Vue 3.0.0 and the Vue CLI

Other versions available:

In this tutorial we'll cover how to implement Facebook Login in Vue 3 with an example app that allows you to login with Facebook and view/update/delete accounts registered in the Vue app.

The first time you login with Facebook an account is registered in the Vue app with your Facebook id so it can identify you when you login again with Facebook. The account is created with the name from your Facebook account and an extraInfo field with some default text, both the name and extra info can be updated in the Vue app, and updating account details only changes them in the app it doesn't affect anything on Facebook.

The example project is available on GitHub at https://github.com/cornflourblue/vue-3-facebook-login-example.

Here it is in action: (See on CodeSandbox at https://codesandbox.io/s/vue-3-facebook-login-example-l9tkc)


Vue 3 Facebook Login App Details

The example app contains the following three routes (pages) to demonstrate logging in with Facebook, viewing accounts and updating account details:

  • Login (/login) - contains a Facebook login button that triggers authentication with Facebook and registration/authentication with the Vue.js app.
  • Home (/) - displays a list of all accounts in the Vue app with buttons to edit or delete any of them.
  • Edit Account (/edit/:id) - contains a form to update the specified account details.

Facebook App is required for Facebook Login

To integrate Facebook Login into a website or application you need to create a Facebook App at https://developers.facebook.com/apps/ and set up the Facebook Login product under the App. Creating a Facebook App will provide you with a Facebook App ID which is required when initializing the Facebook JavaScript SDK (FB.init(...)). For more info see the Facebook Login docs at https://developers.facebook.com/docs/facebook-login/web.

The example Vue 3 app uses a Facebook App named JasonWatmore.com Login Example that I created for this tutorial (App ID: 314930319788683). The Facebook App ID is located in the dotenv file (/.env) in the example, environment variables set in the dotenv file that are prefixed with VUE_APP_ are accessible in the Vue app via process.env.<variable name> (e.g. process.env.VUE_APP_FACEBOOK_APP_ID). For more info on using environment variables in Vue see https://cli.vuejs.org/guide/mode-and-env.html#environment-variables.

Fake backend API

The example Vue 3 app runs with a fake backend api by default to enable it to run completely in the browser without a real api (backend-less), the fake api contains routes for authentication and account CRUD operations and it uses browser local storage to save data. To disable the fake backend you just have to remove a couple of lines of code from the main.js file, you can refer to the fake-backend to see what's required to build a real api for the example.

Updates only affect data in the Vue 3 app

Updating or deleting account information in the Vue 3 app will only change the data saved in the app, it won't (and can't) change anything in the associated Facebook account.

Authentication flow with Facebook access tokens and JWT tokens

Authentication is implemented with Facebook access tokens and JWT tokens. On successful login to Facebook an access token is returned to the Vue 3 app, which is then used to authenticate with the api (or fake backend) which returns a short lived JWT token that expires after 15 minutes. The JWT is used for accessing secure routes on the api, and the Facebook access token is used to re-authenticate with the api to get a new JWT token when (or just before) it expires. The Vue app starts a timer to re-authenticate for a new JWT token 1 minute before it expires to keep the account logged in, this is done in the apiAuthenticate() method of the account service.


Run the Vue 3 Facebook Login App Locally

  1. Install Node.js and npm from https://nodejs.org
  2. Download or clone the project source code from https://github.com/cornflourblue/vue-3-facebook-login-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Start the application in SSL (https) mode by running npm run serve:ssl from the command line in the project root folder, SSL is required for the Facebook SDK to run properly, this will launch a browser with the URL https://localhost:8080/.
  5. You should see the message Your connection is not private (or something similar in non Chrome browsers), this is nothing to worry about it's just because the Vue development server runs with a self signed SSL certificate. To open the app click the "Advanced" button and the link "Proceed to localhost".


Vue 3 Project Structure

The Vue CLI was used to generate the base project structure with the vue create <project name> command, the CLI is also used to build and serve the application. For more info about the Vue CLI see https://cli.vuejs.org.

Each feature has it's own folder (home & login), other shared/common code such as services, helpers etc are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure.

The index.js files in the shared/non-feature folders are barrel files that group the exported modules from a folder together so they can be imported using the folder path instead of the full module path and to enable importing multiple modules in a single import (e.g. import { initFacebookSdk, jwtInterceptor, errorInterceptor, router } from './_helpers';).

Here are the main project files that contain the application logic.

 

Main Index Html File

Path: /public/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. The Vue CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <title><%= htmlWebpackPlugin.options.title %></title>

        <!-- bootstrap & font-awesome css -->
        <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
        <link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
    </head>
    <body>
        <div id="app">Loading...</div>

        <noscript>
            <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
        </noscript>
        <!-- built files will be auto injected -->
    </body>
</html>
 

Auth Guard

Path: /src/_helpers/auth.guard.js

The auth guard is a Vue Router Navigation Guard function that prevents unauthenticated users from accessing restricted routes. If the function returns true the navigation is confirmed and allowed to proceed, otherwise if the function returns false the navigation is cancelled.

The auth guard uses the account service to check if the user is logged in, if they are the route is confirmed, otherwise the route is cancelled and the user is redirected to the /login page with the returnUrl in the query parameters.

Vue router navigation guards are attached to routes in the router config, this auth guard is used in router.js to protect the home and edit account routes.

import { accountService } from '@/_services';
import { router } from '@/_helpers';

export function authGuard(to) {
    const account = accountService.accountValue;
    if (account) {
        // logged in so return true
        return true;
    }

    // not logged in so redirect to login page with the return url
    router.push({ path: '/login', query: { returnUrl: to.fullPath } });
    return false;
}
 

Error Interceptor

Path: /src/_helpers/error.interceptor.js

The Error Interceptor intercepts http responses from the api to check if there were any errors. All errors are logged to the console and if there is a 401 Unauthorized or 403 Forbidden response the account is automatically logged out of the application.

It's implemented as an axios response interceptor, by passing callback functions to axios.interceptors.response.use() you can intercept responses before they are handled by then() or catch(). The first callback function intercepts successful responses and the second callback function intercepts error responses. For more info on axios interceptors see https://github.com/axios/axios#interceptors.

The error interceptor is initialized on app startup in the main.js file.

import axios from 'axios';

import { accountService } from '@/_services';

export function errorInterceptor() {
    axios.interceptors.response.use(null, (error) => {
        const { response } = error;
        if (!response) {
            // network error
            console.error(error);
            return;
        }
    
        if ([401, 403].includes(response.status) && accountService.accountValue) {
            // auto logout if 401 or 403 response returned from api
            accountService.logout();
        }

        const errorMessage = response.data?.message || response.statusText;
        console.error('ERROR:', errorMessage);
    });
}
 

Fake Backend

Path: /src/_helpers/fake-backend.js

In order to run and test the Vue 3 app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Vue app and sends back "fake" responses. This is done by monkey patching the axios request methods (get, post, put, delete) to return fake responses for a specific set of routes.

Monkey patching is a technique used to alter the behaviour of an existing function either to extend it or change the way it works. In JavaScript this is done by storing a reference to the original function in a variable and replacing the original function with a new custom function that (optionally) calls the original function before/after executing some custom code.

The fake backend is organised into a top level handleRoute() function that checks the request url and method to determine how the request should be handled. For fake routes one of the below // route functions is called, for all other routes the request is passed through to the real backend by calling the original axios request function (axios[`original${method}`](url, body())). Below the route functions there are // helper functions for returning different response types and performing small tasks.

import axios from 'axios';

import { accountService } from '@/_services';

// array in local storage for accounts
const accountsKey = 'vue-3-facebook-login-accounts';
let accounts = JSON.parse(localStorage.getItem(accountsKey)) || [];

export function fakeBackend() {
    const methods = ['get', 'post', 'put', 'delete'];
    methods.forEach(method => {
        axios[`original${method}`] = axios[method];
        axios[method] = function (url, ...params) {
            return new Promise((resolve, reject) => {
                return handleRoute();

                function handleRoute() {
                    switch (true) {
                        case url.endsWith('/accounts/authenticate') && method === 'post':
                            return authenticate();
                        case url.endsWith('/accounts') && method === 'get':
                            return getAccounts();
                        case url.match(/\/accounts\/\d+$/) && method === 'get':
                            return getAccountById();
                        case url.match(/\/accounts\/\d+$/) && method === 'put':
                            return updateAccount();
                        case url.match(/\/accounts\/\d+$/) && method === 'delete':
                            return deleteAccount();
                        default:
                            // pass through any requests not handled above
                            return axios[`original${method}`](url, body())
                                .then(response => resolve(response))
                                .catch(error => reject(error));
                    }
                }

                // route functions

                function authenticate() {
                    const { accessToken } = body();

                    axios.get(`https://graph.facebook.com/v8.0/me?access_token=${accessToken}`)
                        .then(response => {
                            const { data } = response;
                            if (data.error) return unauthorized(data.error.message);

                            let account = accounts.find(x => x.facebookId === data.id);
                            if (!account) {
                                // create new account if first time logging in
                                account = {
                                    id: newAccountId(),
                                    facebookId: data.id,
                                    name: data.name,
                                    extraInfo: `This is some extra info about ${data.name} that is saved in the API`
                                }
                                accounts.push(account);
                                localStorage.setItem(accountsKey, JSON.stringify(accounts));
                            }

                            return ok({
                                ...account,
                                token: generateJwtToken(account)
                            });
                        });
                }
    
                function getAccounts() {
                    if (!isLoggedIn()) return unauthorized();
                    return ok(accounts);
                }

                function getAccountById() {
                    if (!isLoggedIn()) return unauthorized();

                    let account = accounts.find(x => x.id === idFromUrl());
                    return ok(account);
                }

                function updateAccount() {
                    if (!isLoggedIn()) return unauthorized();

                    let params = body();
                    let account = accounts.find(x => x.id === idFromUrl());

                    // update and save account
                    Object.assign(account, params);
                    localStorage.setItem(accountsKey, JSON.stringify(accounts));

                    return ok(account);
                }

                function deleteAccount() {
                    if (!isLoggedIn()) return unauthorized();

                    // delete account then save
                    accounts = accounts.filter(x => x.id !== idFromUrl());
                    localStorage.setItem(accountsKey, JSON.stringify(accounts));
                    return ok();
                }

                // helper functions
    
                function ok(body) {
                    // wrap in timeout to simulate server api call
                    setTimeout(() => resolve({ status: 200, data: body }), 500);
                }
    
                function unauthorized() {
                    setTimeout(() => {
                        const response = { status: 401, data: { message: 'Unauthorized' } };
                        reject(response);
                        
                        // manually trigger error interceptor
                        const errorInterceptor = axios.interceptors.response.handlers[0].rejected;
                        errorInterceptor({ response });
                    }, 500);
                }
    
                function isLoggedIn() {
                    return accountService.accountValue;
                }

                function idFromUrl() {
                    const urlParts = url.split('/');
                    return parseInt(urlParts[urlParts.length - 1]);
                }

                function body() {
                    if (['post', 'put'].includes(method))
                        return params[0];
                }
                
                function newAccountId() {
                    return accounts.length ? Math.max(...accounts.map(x => x.id)) + 1 : 1;
                }
    
                function generateJwtToken(account) {
                    // create token that expires in 15 minutes
                    const tokenPayload = { 
                        exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),
                        id: account.id
                    }
                    return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
                }
            });
        }
    });
}
 

Init Facebook SDK

Path: /src/_helpers/init-facebook-sdk.js

The init Facebook SDK function runs before the Vue 3 app starts up in main.js, it loads and initializes the Facebook SDK and gets the user's login status from Facebook. If the user is already logged in with Facebook they are automatically logged into the Vue app using the Facebook access token and taken to the home page, otherwise the app starts normally and displays the login page.

import { accountService } from '@/_services';

const facebookAppId = process.env.VUE_APP_FACEBOOK_APP_ID;

export function initFacebookSdk() {
    return new Promise(resolve => {
        // wait for facebook sdk to initialize before starting the vue app
        window.fbAsyncInit = function () {
            FB.init({
                appId: facebookAppId,
                cookie: true,
                xfbml: true,
                version: 'v8.0'
            });

            // auto authenticate with the api if already logged in with facebook
            FB.getLoginStatus(({ authResponse }) => {
                if (authResponse) {
                    accountService.apiAuthenticate(authResponse.accessToken).then(resolve);
                } else {
                    resolve();
                }
            });
        };

        // load facebook sdk script
        (function (d, s, id) {
            var js, fjs = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) { return; }
            js = d.createElement(s); js.id = id;
            js.src = "https://connect.facebook.net/en_US/sdk.js";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));    
    });
}
 

JWT Interceptor

Path: /src/_helpers/jwt.interceptor.js

The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request is to the Vue app's api url (process.env.VUE_APP_API_URL).

It's implemented as an axios request interceptor, by passing a callback function to axios.interceptors.request.use() you can intercept requests before they get sent to the server. For more info on axios interceptors see https://github.com/axios/axios#interceptors.

The jwt interceptor is initialized on app startup in the main.js file.

import axios from 'axios';

import { accountService } from '@/_services';

export function jwtInterceptor() {
    axios.interceptors.request.use(request => {
        // add auth header with jwt if account is logged in and request is to the api url
        const account = accountService.accountValue;
        const isLoggedIn = account?.token;
        const isApiUrl = request.url.startsWith(process.env.VUE_APP_API_URL);

        if (isLoggedIn && isApiUrl) {
            request.headers.common.Authorization = `Bearer ${account.token}`;
        }

        return request;
    });
}
 

Router

Path: /src/_helpers/router.js

The router defines the routes for the Vue 3 application and creates a new Vue Router instance with the createRouter() function. The exported router instance is imported into main.js where it is passed to the Vue app on startup.

The createRouter() function is part of the Vue Router v4 which is compatible with Vue 3, the previous version of the router (Vue Router v3) is compatible with Vue 2. For more info on what's changed in the new version of the Vue Router see https://next.router.vuejs.org/guide/migration/.

The home route maps the root path (/) of the app to the home component, the edit account route /edit/:id maps to the edit account component, and the /login route maps to the login component.

The home and edit account routes are secured by passing the auth guard to the beforeEnter property of each route.

The linkActiveClass: 'active' parameter passed to the router sets the active CSS class for router-link components to active, this is so the nav bar links in the example show as active with Bootstrap CSS (the default class is router-link-active).

For more information on Vue routing see https://router.vuejs.org/.

import { createRouter, createWebHistory} from 'vue-router';

import Home from '@/home/Home';
import EditAccount from '@/home/EditAccount';
import Login from '@/login/Login';
import { authGuard } from '@/_helpers';

const routes = [
    { path: '/', component: Home, beforeEnter: authGuard },
    { path: '/edit/:id', component: EditAccount, beforeEnter: authGuard },
    { path: '/login', component: Login },

    // otherwise redirect to home
    { path: '/:pathMatch(.*)*', redirect: '/' }
];

export const router = createRouter({
    history: createWebHistory(),
    routes
});
 

Account Service

Path: /src/_services/account.service.js

The account service handles communication between the Vue 3 app and the backend api for everything related to accounts. It contains methods for logging in and out, as well as standard CRUD methods for retrieving and modifying account data.

The login() method first logs into Facebook (await new Promise(FB.login)), then passes the Facebook access token to the apiAuthenticate() method to login to the api (or fake backend).

On successful login the api returns the account details and a JWT token which are published to all subscriber components with the call to accountSubject.next(account) in the apiAuthenticate() method. The method then starts a countdown timer by calling startAuthenticateTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires in order to keep the account logged in.

The logout() method revokes the Facebook App's permissions with FB.api('/me/permissions', 'delete') then logs out of Facebook by calling FB.logout(), revoking permissions is required to completely logout because FB.logout() doesn't remove the FB auth cookie so the user is logged back in on page refresh. The logout() method then cancels the silent refresh running in the background by calling stopAuthenticateTimer(), logs the user out of the Vue app by publishing a null value to all subscriber components (accountSubject.next(null)) and redirects to the login page.

The account property exposes an RxJS observable (Observable<Account>) so any component can subscribe to be notified when a user logs in, logs out, has their token refreshed or updates their account. The notification is triggered by the call to accountSubject.next() from each of the corresponding methods in the service. For more info on component communication with RxJS see Vue.js + RxJS - Communicating Between Components with Observable & Subject.

import { BehaviorSubject } from 'rxjs';
import axios from 'axios';

import { router } from '@/_helpers';

const baseUrl = `${process.env.VUE_APP_API_URL}/accounts`;
const accountSubject = new BehaviorSubject(null);

export const accountService = {
    login,
    apiAuthenticate,
    logout,
    getAll,
    getById,
    update,
    delete: _delete,
    account: accountSubject.asObservable(),
    get accountValue () { return accountSubject.value; }
};

async function login() {
    // login with facebook then authenticate with the API to get a JWT auth token
    const { authResponse } = await new Promise(FB.login);
    if (!authResponse) return;

    await apiAuthenticate(authResponse.accessToken);

    // get return url from query parameters or default to home page
    const returnUrl = router.currentRoute.value.query['returnUrl'] || '/';
    router.push(returnUrl);
}

async function apiAuthenticate(accessToken) {
    // authenticate with the api using a facebook access token,
    // on success the api returns an account object with a JWT auth token
    const response = await axios.post(`${baseUrl}/authenticate`, { accessToken });
    const account = response.data;
    accountSubject.next(account);
    startAuthenticateTimer();
    return account;
}

function logout() {
    // revoke app permissions to logout completely because FB.logout() doesn't remove FB cookie
    FB.api('/me/permissions', 'delete', null, () => FB.logout());
    stopAuthenticateTimer();
    accountSubject.next(null);
    router.push('/login');
}

function getAll() {
    return axios.get(baseUrl)
        .then(response => response.data);
}

function getById(id) {
    return axios.get(`${baseUrl}/${id}`)
        .then(response => response.data);
}

async function update(id, params) {
    const response = await axios.put(`${baseUrl}/${id}`, params);
    let account = response.data;
    // update the current account if it was updated
    if (account.id === accountSubject.value?.id) {
        // publish updated account to subscribers
        account = { ...accountSubject.value, ...account };
        accountSubject.next(account);
    }
    return account;
}

async function _delete(id) {
    await axios.delete(`${baseUrl}/${id}`);
    if (id === accountSubject.value?.id) {
        // auto logout if the logged in account was deleted
        logout();
    }
}

// helper methods

let authenticateTimeout;

function startAuthenticateTimer() {
    // parse json object from base64 encoded jwt token
    const jwtToken = JSON.parse(atob(accountSubject.value.token.split('.')[1]));

    // set a timeout to re-authenticate with the api one minute before the token expires
    const expires = new Date(jwtToken.exp * 1000);
    const timeout = expires.getTime() - Date.now() - (60 * 1000);
    const { accessToken } = FB.getAuthResponse();
    authenticateTimeout = setTimeout(() => apiAuthenticate(accessToken), timeout);
}

function stopAuthenticateTimer() {
    // cancel timer for re-authenticating with the api
    clearTimeout(authenticateTimeout);
}
 

Edit Account Component

Path: /src/home/EditAccount.vue

The component template contains a form for updating the details of an account.

The component code contains the account, loading and error variables, and the handleSubmit method that are referenced in the template. The variables and method are created and returned by the Vue 3 setup() method which makes them available to the template. The setup() method runs before the component is created and serves as the entry point for Vue 3 components that are created with the new Composition API. The variables are created as a reactive variables using the Vue 3 ref() function so the template will automatically update (re-render) when their values change.

In the setup() method the component fetches the specified account details with the account service (accountService.getById(id)) to pre-populate the form field values.

On submit the handleSubmit() method updates the account with the account service (accountService.update(id, account.value)) and redirects the user back to the home page.

<template>
    <h2>Edit Account</h2>
    <p>Updating the information here will only change it inside this application, it won't (and can't) change anything in the associated Facebook account.</p>
    <form v-if="account" @submit.prevent="handleSubmit">
        <div class="form-group">
            <label>Facebook Id</label>
            <div>{{account.facebookId}}</div>
        </div>
        <div class="form-group">
            <label>Name</label>
            <input type="text" v-model="account.name" class="form-control" />
        </div>
        <div class="form-group">
            <label>Extra Info</label>
            <input type="text" v-model="account.extraInfo" class="form-control" />
        </div>
        <div class="form-group">
            <button type="submit" :disabled="loading" class="btn btn-primary">
                <span v-if="loading" class="spinner-border spinner-border-sm mr-1"></span>
                Save
            </button>
            <router-link to="../../" class="btn btn-link">Cancel</router-link>
            <div v-if="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
        </div>
    </form>
    <div v-if="!account" class="text-center p-3">
        <span class="spinner-border spinner-border-lg align-center"></span>
    </div>
</template>

<script>
import { ref } from 'vue';
import { useRoute } from 'vue-router';

import { router } from '@/_helpers';
import { accountService } from '@/_services';

export default {
    setup() {
        const route = useRoute();
        const account = ref();
        const id = route.params.id;
        accountService.getById(id)
            .then(x => account.value = x);

        const loading = ref(false);
        const error = ref('');
        const handleSubmit = () => {
            loading.value = true;
            error.value = '';
            accountService.update(id, account.value)
                .then(() => {
                    router.push('../');
                })
                .catch(err => {
                    error.value = err;
                    loading.value = false;
                });
        }

        return {
            account,
            loading,
            error,
            handleSubmit
        }
    }
}
</script>
 

Home Component

Path: /src/home/Home.vue

The home component template contains a simple welcome message and a list of all accounts with buttons for editing or deleting.

The home component code gets all accounts from the account service in the setup() method and makes them available to the template via the accounts reactive property.

The deleteAccount() method sets the property isDeleting to true for the specified account so the template displays a spinner on the delete button, then calls accountService.delete(id) to delete the account and removes the deleted account from component accounts array so it is removed from the UI.

<template>
    <h2>You're logged in with Vue 3 & Facebook!!</h2>
    <p>All accounts from secure api end point:</p>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Id</th>
                <th>Facebook Id</th>
                <th>Name</th>
                <th>Extra Info</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="account in accounts" :key="account.id">
                <td>{{account.id}}</td>
                <td>{{account.facebookId}}</td>
                <td>{{account.name}}</td>
                <td>{{account.extraInfo}}</td>
                <td class="text-right" style="white-space: nowrap">
                    <router-link :to="`/edit/${account.id}`" class="btn btn-sm btn-primary mr-1">Edit</router-link>
                    <button @click="deleteAccount(account.id)" class="btn btn-sm btn-danger btn-delete-account" :disabled="account.isDeleting">
                        <span v-if="account.isDeleting" class="spinner-border spinner-border-sm"></span>
                        <span v-else>Delete</span>
                    </button>
                </td>
            </tr>
            <tr v-if="!accounts">
                <td colspan="5" class="text-center">
                    <span class="spinner-border spinner-border-lg align-center"></span>
                </td>
            </tr>
        </tbody>
    </table>
</template>

<script>
import { ref } from 'vue';

import { accountService } from '@/_services';

export default {
    setup() {
        const accounts = ref();
        accountService.getAll()
            .then(x => accounts.value = x);

        const deleteAccount = (id) => {
            const account = accounts.value.find(x => x.id === id);
            account.isDeleting = true;
            accountService.delete(id)
                .then(() => accounts.value = accounts.value.filter(x => x.id !== id));
        };

        return {
            accounts,
            deleteAccount
        };
    }
}
</script>
 

Login Component

Path: /src/login/Login.vue

The login component template contains a single Facebook login button that is bound to the login method on click.

The login component code maps the login method to the accountService.login method which logs into the application using Facebook. If the user is already logged in they are automatically redirected to the home page.

<template>
    <div class="col-md-6 offset-md-3 mt-5 text-center">
        <div class="card">
            <h4 class="card-header">Vue 3 Facebook Login Example</h4>
            <div class="card-body">
                <button class="btn btn-facebook" @click="login">
                    <i class="fa fa-facebook mr-1"></i>
                    Login with Facebook
                </button>
            </div>
        </div>
    </div>
</template>

<script>
import { router } from '@/_helpers';
import { accountService } from '@/_services';

export default {
    setup() {
        // redirect to home if already logged in
        if (accountService.accountValue) {
            router.push('/');
        }

        return {
            login: accountService.login
        };
    }
}
</script>
 

App Component

Path: /src/App.vue

The app component template contains the main nav bar which is only displayed for authenticated accounts, and a router-view component for displaying the contents of each view based on the current route / path.

The app component code contains the account variable and logout method that are referenced in the template. The variable and method are created and returned by the Vue 3 setup() method which makes them available to the template. The setup() method runs before the component is created and serves as the entry point for Vue 3 components that are created with the new Composition API. The account variable is created as a reactive variable using the Vue 3 ref() function so the template will automatically update (re-render) when the variable value changes.

In the setup() method the component subscribes to the account observable of the account service so it can reactively show/hide the main nav bar in the template when the user logs in/out of the application. I didn't worry about unsubscribing from the observable here because it's the root component of the application and will only be destroyed when the Vue app is closed.

The logout() method points to the accountService.logout() method and is called from the logout link in the main nav bar to log the account out and redirect to the login page.

<template>
    <!-- nav -->
    <nav class="navbar navbar-expand navbar-dark bg-dark" v-if="account">
        <div class="navbar-nav">
            <router-link to="/" class="nav-item nav-link">Home</router-link>
            <a class="nav-item nav-link" @click="logout">Logout</a>
        </div>
    </nav>

    <!-- main app container -->
    <div class="container pt-4">
        <router-view></router-view>
    </div>
</template>

<script>
import { ref } from 'vue';

import { accountService } from '@/_services';

export default {
    setup() {
        const account = ref(null);
        accountService.account.subscribe(x => account.value = x);

        return {
            account,
            logout: accountService.logout
        }
    }
}
</script>
 

Main.js

Path: /src/main.js

The main.js file bootstraps the Vue application by mounting the App component in the #app div element defined in the main index html file above.

Before starting the Vue app it imports the global LESS/CSS stylesheet into the application, enables the fake backend api, enables the jwt interceptor and error interceptor, and waits for the Facebook SDK to load and initialize.

To disable the fake backend simply remove the 2 lines below the comment // setup fake backend.

import { createApp } from 'vue';

// global stylesheet
import './styles.less';

// setup fake backend
import { fakeBackend } from './_helpers';
fakeBackend();

import { initFacebookSdk, jwtInterceptor, errorInterceptor, router } from './_helpers';
import App from './App.vue';

// enable interceptors for http requests
jwtInterceptor();
errorInterceptor();

// wait for facebook sdk to start app
initFacebookSdk().then(startApp);

function startApp() {
    createApp(App)
        .use(router)
        .mount('#app');
}
 

Global LESS/CSS Styles

Path: /src/styles.less

The global styles file contains LESS/CSS styles that are applied globally throughout the Vue 3 application.

a { cursor: pointer }

.btn-facebook {
    background: #3B5998;
    color: #fff;

    &:hover {
        color: #fff;
        opacity: 0.8;
    }
}

.btn-delete-account {
    width: 40px;
    text-align: center;
    box-sizing: content-box;
}
 

dotenv

Path: /.env

The dotenv file contains environment variables used in the example Vue 3 app.

Environment variables set in the dotenv file that are prefixed with VUE_APP_ are accessible in the Vue app via process.env.<variable name> (e.g. process.env.VUE_APP_FACEBOOK_APP_ID). For more info on using environment variables in Vue see https://cli.vuejs.org/guide/mode-and-env.html#environment-variables

VUE_APP_API_URL=http://localhost:4000
VUE_APP_FACEBOOK_APP_ID=314930319788683
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies that get installed when you run npm install and scripts that are executed when you run npm run serve:ssl or npm run build etc. Full documentation is available at https://docs.npmjs.com/files/package.json.

{
    "name": "vue-3-facebook-login-example",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "serve": "vue-cli-service serve --open",
        "serve:ssl": "vue-cli-service serve --https --open --public localhost:8080",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
    },
    "dependencies": {
        "axios": "^0.20.0",
        "core-js": "^3.6.5",
        "rxjs": "^6.6.3",
        "vue": "^3.0.0-0",
        "vue-router": "^4.0.0-beta.12"
    },
    "devDependencies": {
        "@vue/cli-plugin-babel": "~4.5.0",
        "@vue/cli-plugin-eslint": "~4.5.0",
        "@vue/cli-service": "~4.5.0",
        "@vue/compiler-sfc": "^3.0.0-0",
        "babel-eslint": "^10.1.0",
        "eslint": "^6.7.2",
        "eslint-plugin-vue": "^7.0.0-0",
        "less": "^3.12.2",
        "less-loader": "^7.0.1"
    },
    "eslintConfig": {
        "root": true,
        "env": {
            "node": true
        },
        "extends": [
            "plugin:vue/vue3-essential",
            "eslint:recommended"
        ],
        "parserOptions": {
            "parser": "babel-eslint"
        },
        "rules": {},
        "globals": {
            "FB": "readonly"
        }
    },
    "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead"
    ]
}

 


Need Some Vue 3 Help?

Search fiverr for freelance Vue 3 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by