Node.js : Koa, Firebase et Firestore gestion de vos offres commerciales (Partie 1)

Node.js : Koa, Firebase et Firestore gestion de vos offres commerciales (Partie 1)
Categories:
Posté le

Dans le cadre de notre activité ou celle de nos clients, la gestion des offres commerciales (offres spéciales, promotions, ...) éphémères ou non est une problématique qui revient presque systématiquement.

La problématique est la suivante : Comment gérer la visibilité et la mise en avant de nos offres sur les différents sites  et plateformes de l'entreprise ?

Notre solution : réalisation d'une plateforme de gestion des offres avec mise en avant par différents moyens (iframe, API, ...)

Cette plateforme permettra donc la gestion :

  • Ajout/Édition/Suppression d'offres
  • Mise en avant d'une offre
  • Intégration iframe d'offres
  • API REST des offres

Les différentes solutions techniques pour le développement et l'hébergement de cette plateforme :

Les sources du projet : https://github.com/Zenicheck/blog/tree/master/active-offer-koa-v1

Mise en place du projet :

Installation des dépendances :

$ npm init
$ npm i koa @koa/router bcrypt dotenv firebase-admin koa-ejs koa-logger koa-static

Modification du package.json :

/* package.json */
/*
** ...
*/
"type": "module",
  "scripts": {
    "start": "node --experimental-modules --experimental-json-modules bin/www.mjs"
}
/*
** ...
*/

Création du .env (en ajoutant l'host de votre application Firebase, pour cela il vous suffit de suivre https://firebase.google.com/docs/admin/setup) :

# .env
USERNAME=admin
PASSWORD=$2b$10$AH2rSG36c7XF.LTVDWatv.4Ga/zr72V6GDGdXA9imnKxXJLjNnkJe
GEN_SALT=10
FIREBASE_HOST=https://-**********._firebaseio.com

Le mot de passe a été généré de la manière suivante :

$ node
> const bcrypt = require('bcrypt')
> bcrypt.hash('_admin', 10).then(console.log)

Script de lancement pendant le développement :

#!/bin/sh

/* run.sh */

PORT=8900 npx nodemon --experimental-modules --experimental-json-modules bin/www.mjs --watch routes --watch views --watch helpers --watch app.mjs

Téléchargez SB Admin 2  (https://startbootstrap.com/themes/sb-admin-2/) et mettez les assets dans le dossier public de l'application :

$ unzip startbootstrap-sb-admin-2-gh-pages.zip -d template
$ cd template
$ cd startbootstrap-sb-admin-2-gh-pages
$ mv * ..
$ mv .* ..
$ cd ..
$ rmdir  startbootstrap-sb-admin-2-gh-pages
$ cd ..
$ mkdir public
$ cp -r template/img template/vendor template/css template/js public

Bootstrap du code applicatif :

/* bin/www.mjs */
import http from 'http'

import app from '../app.mjs'

http.createServer(app.callback()).listen(process.env.PORT || 3000)

Il est ici important de bien spécifier votre emplacement du fichier des clefs de firebase ici './offer-firebase.json'.

/* app.mjs */
import path from 'path'
import Koa from 'koa'
import ejs from 'koa-ejs'
import logger from 'koa-logger'
import _static from 'koa-static'
import admin from 'firebase-admin'
import dotenv from 'dotenv'

import indexRouter from './routes/index.mjs'
import admRouter from './routes/adm.mjs'

const __dirname = path.resolve()

const config = dotenv.config()

import serviceAccount from './offer-firebase.json'

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: config.FIREBASE_HOST
})

const db = admin.firestore()

const app = new Koa()

app.context.db = db
app.context.config = config
app.context.title = 'Gestion de vos offres commerciales'

ejs(app, {
    root: path.join(__dirname, 'views'),
    layout: 'template',
    viewExt: 'html.ejs',
    cache: false, //TODO turn on for production
    debug: true, //TODO turn off for profuction
})

app.use(logger())

app.use(_static(path.join(__dirname, 'public')))

app.use(indexRouter.routes())
app.use(indexRouter.allowedMethods())

app.use(admRouter.routes())
app.use(admRouter.allowedMethods())


export default app

Les différents fichiers des routes :

/* routes/index.mjs */
import Router from '@koa/router'

const router = new Router({ prefix: '/public' })

router.get('/', async ctx => await ctx.render('index', { layout: false, ctx, title : 'Offre du moment' }))

export default router
/* routes/adm.mjs */
import Router from '@koa/router'

const router = new Router({ prefix: '/adm' })

router.get('/', async ctx => await ctx.render('adm', { ctx, title: 'Adm' }))

export default router

Il n'y a rien de bien spécifique ici, il s'agit simplement de la mise en place de l'architecture de l'application avec l'initialisation de firebase et de ejs comme moteur de template.

Création des templates et vues :

Le template pour toutes les vues de la partie administration :

<!DOCTYPE html>
<html lang="fr">
<!-- views/template.html.ejs -->
<head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="Zenicheck - Gestion de vos offres commerciales">
    <meta name="author" content="Yann Carlen - Zenicheck">

    <title><%= `${ctx.title}-${title}` %></title>

    <!-- Custom fonts for this template-->
    <link href="/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
    <link
        href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
        rel="stylesheet">

    <!-- Custom styles for this template-->
    <link href="/css/sb-admin-2.min.css" rel="stylesheet">

</head>

<body id="page-top">


    <!-- Page Wrapper -->
    <div id="wrapper">

        <!-- Sidebar -->
        <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">

            <!-- Sidebar - Brand -->
            <a class="sidebar-brand d-flex align-items-center justify-content-center" href="/adm">
                <div class="sidebar-brand-icon rotate-n-15">
                    <i class="fas fa-ad"></i>
                </div>
                <div class="sidebar-brand-text mx-3">ZENICHECK <sup>Offres</sup></div>
            </a>

            <!-- Divider -->
            <hr class="sidebar-divider my-0">

            <!-- Nav Item - Dashboard -->
            <li class="nav-item active">
                <a class="nav-link" href="/adm">
                    <i class="fas fa-fw fa-tachometer-alt"></i>
                    <span>Dashboard</span></a>
            </li>

            <!-- Divider -->
            <hr class="sidebar-divider">

            <!-- Heading -->
            <div class="sidebar-heading">
                Offres
            </div>

            <!-- Nav Item - Offers -->
            <li class="nav-item">
                <a class="nav-link" href="/adm/offer">
                    <i class="fas fa-fw fa-ad"></i>
                    <span>offres</span></a>
            </li>

            <!-- Divider -->
            <hr class="sidebar-divider d-none d-md-block">

            <!-- Sidebar Toggler (Sidebar) -->
            <div class="text-center d-none d-md-inline">
                <button class="rounded-circle border-0" id="sidebarToggle"></button>
            </div>

        </ul>
        <!-- End of Sidebar -->

        <!-- Content Wrapper -->
        <div id="content-wrapper" class="d-flex flex-column">

            <!-- Main Content -->
            <div id="content">

                <!-- Topbar -->
                <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">

                    <!-- Sidebar Toggle (Topbar) -->
                    <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
                        <i class="fa fa-bars"></i>
                    </button>

                    <!-- Topbar Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Nav Item - User Information -->
                        <li class="nav-item dropdown no-arrow">
                            <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
                                data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <span class="mr-2 d-none d-lg-inline text-gray-600 small">Admin</span>
                            </a>
                            <!-- Dropdown - User Information -->
                            <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
                                aria-labelledby="userDropdown">
                                <a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
                                    <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                                    Se déconnecter
                                </a>
                            </div>
                        </li>

                    </ul>

                </nav>
                <!-- End of Topbar -->

                <!-- Begin Page Content -->
                <div class="container-fluid">
                    <%- body %>
                </div>
                <!-- /.container-fluid -->

            </div>
            <!-- End of Main Content -->

            <!-- Footer -->
            <footer class="sticky-footer bg-white">
                <div class="container my-auto">
                    <div class="copyright text-center my-auto">
                        <span>Copyright &copy; Réalisé par <a href="https://yanncarlen.com" target="_blank">Yann
                                Carlen</a>,
                            2019 - 2020 <a href="https://zenicheck.com" target="_blank">ZENICHECK</a></span>
                    </div>
                </div>
            </footer>
            <!-- End of Footer -->

        </div>
        <!-- End of Content Wrapper -->

    </div>
    <!-- End of Page Wrapper -->

    <!-- Scroll to Top Button-->
    <a class="scroll-to-top rounded" href="#page-top">
        <i class="fas fa-angle-up"></i>
    </a>

    <!-- Logout Modal-->
    <div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
        aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">Vraiment ?</h5>
                    <button class="close" type="button" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div class="modal-body">Voulez-vous vraiment vous déconnecter ?</div>
                <div class="modal-footer">
                    <button class="btn btn-secondary" type="button" data-dismiss="modal">Annuler</button>
                    <a class="btn btn-primary" href="/adm/logout">Se déconnecter</a>
                </div>
            </div>
        </div>
    </div>

    <!-- Bootstrap core JavaScript-->
    <script src="/vendor/jquery/jquery.min.js"></script>
    <script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>

    <!-- Core plugin JavaScript-->
    <script src="/vendor/jquery-easing/jquery.easing.min.js"></script>

    <!-- Custom scripts for all pages-->
    <script src="/js/sb-admin-2.min.js"></script>

    <!-- Page level plugins -->
    <script src="/vendor/chart.js/Chart.min.js"></script>
</body>

</html>

La vue d'admin :

<!--- views/adm.html.ejs -->
<%- include('title', { title }); %>
<!-- Content Row -->
<div class="row">
</div>

La vue qui servira d'iframe :

<!-- views/index.html.ejs -->
<%- include('title', { title }); %>

<!-- Content Row -->
<div class="row">
</div>

Le block title :

<!-- views/title.html.ejs -->
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
    <h1 class="h3 mb-0 text-gray-800"><%= title %></h1>
</div>

Il est également important d'ajouter le fichier credential de firebase à votre .gitignore :

# .gitignore
# ...

# Firebase
offer-firebase.json

# ...
Premier visuel :

Une fois ces différentes étapes suivies, vous devriez obtenir le rendu premier rendu suivant :

Les sources du projet : https://github.com/Zenicheck/blog/tree/master/active-offer-koa-v1 

Dans la prochaine partie, il sera l'occasion de créer le système de CRUD autour des offres ainsi que le système de login/mot de passe (simplifié pour l'occasion).

Lien vers la partie suivante : https://blog.yanncarlen.com/node-js-koa-firebase-et-firestore-gestion-de-vos-offres-commerciales-partie-2-authentification-crud

L'offre du moment :

N'hésitez pas à me contacter pour demander un devis, discuter d'un projet et surtout profitez de notre offre du moment : 1 an d'hébergement de site internet offert à la signature d'un contrat !  → En savoir plus