From 585e578e482411f04cd9d78993c19d3bed590044 Mon Sep 17 00:00:00 2001
From: Samuel ORTION <samuel.ortion@orange.fr>
Date: Sun, 28 Aug 2022 09:14:02 +0200
Subject: [PATCH] game&api: Added API queries for geolocalized species and
 audio retrieval

---
 .editorconfig                     |  15 --
 .gitignore                        |   7 +-
 README.md                         |  30 ++-
 app.js                            |  49 +++-
 bin/www                           |  18 +-
 controllers/api.js                | 155 ++++++++++++-
 controllers/auth.js               |  48 ++++
 controllers/{home.js => index.js} |   5 +-
 data/.gitkeep                     |   0
 locales/en.js                     |   3 +-
 package-lock.json                 | 373 ++++++++++++++++++++++++++++--
 package.json                      |   6 +
 public/javascripts/api-client.js  |  49 ++++
 public/javascripts/app.js         |  14 +-
 public/javascripts/game.js        |  53 ++++-
 public/javascripts/map.js         |  50 +++-
 public/javascripts/utils.js       |   2 +-
 public/stylesheets/style.css      |   4 +
 routes/api.js                     |  14 ++
 routes/auth.js                    |  11 +
 routes/index.js                   |  13 +-
 utils/choices.js                  |  34 +++
 views/about.pug                   |   8 +-
 views/api.pug                     |   5 +
 views/error.pug                   |   6 +-
 views/game.pug                    |  24 +-
 views/index.pug                   |   2 +-
 views/layout.pug                  |  48 ++--
 28 files changed, 913 insertions(+), 133 deletions(-)
 delete mode 100644 .editorconfig
 create mode 100644 controllers/auth.js
 rename controllers/{home.js => index.js} (62%)
 create mode 100644 data/.gitkeep
 create mode 100644 public/javascripts/api-client.js
 create mode 100644 routes/api.js
 create mode 100644 routes/auth.js
 create mode 100644 utils/choices.js
 create mode 100644 views/api.pug

diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index a850bf9..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,15 +0,0 @@
-# EditorConfig is awesome: https://EditorConfig.org
-
-# top-most EditorConfig file
-root = true
-
-[*]
-indent_style = space
-indent_size = 4
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = false
-insert_final_newline = false
-
-[pug]
-indent_size = 2
diff --git a/.gitignore b/.gitignore
index 8ee743b..939490e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,7 +54,7 @@ typings/
 # Yarn Integrity file
 .yarn-integrity
 
-# dotenv environment variables file
+# dotenv environment constiables file
 .env
 
 # next.js build output
@@ -62,4 +62,7 @@ typings/
 
 # IDE junks
 .vscode
-.ideas
\ No newline at end of file
+.ideas
+
+data/*
+!/data/.gitkeep
\ No newline at end of file
diff --git a/README.md b/README.md
index a5a5541..0f27e3f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,31 @@
 # soundbirder
 
-A web application to play with bird sound identification with data from eBird and Xeno-Canto.
\ No newline at end of file
+A web application to play with bird sound identification with data from eBird and Xeno-Canto.
+
+## Requirements
+
+- NodeJS, NPM
+- MariaDB (MySQL)
+- eBird API key
+
+## Installation
+
+```bash
+git clone https://forge.chapril.org/UncleSamulus/soundbirder
+cd soundbirder
+npm install
+```
+
+Setup the EBIRD_API_KEY in `.env`:
+```text
+EBIRD_API_KEY="secret"
+```
+
+```bash
+npm start
+```
+
+Then go to [http://localhost:3000](http://localhost:3000), and have fun !
+
+
+TODO: add database configuration.
\ No newline at end of file
diff --git a/app.js b/app.js
index ef6a911..d0336fd 100644
--- a/app.js
+++ b/app.js
@@ -1,13 +1,17 @@
-var createError = require('http-errors');
-var express = require('express');
-var path = require('path');
-var cookieParser = require('cookie-parser');
-var logger = require('morgan');
-
-var indexRouter = require('./routes/index');
+const createError = require('http-errors');
+const express = require('express');
+const session = require('express-session');
+const csrf = require('csurf');
+const path = require('path');
+const cookieParser = require('cookie-parser');
+const logger = require('morgan');
 const i18n = require('i18n-2');
 
-var app = express();
+const indexRouter = require('./routes/index');
+const apiRouter = require('./routes/api');
+const authRouter = require('./routes/auth');
+
+const app = express();
 
 // view engine setup
 app.set('views', path.join(__dirname, 'views'));
@@ -19,6 +23,19 @@ app.use(express.urlencoded({ extended: false }));
 app.use(cookieParser());
 app.use(express.static(path.join(__dirname, 'public')));
 
+const sess = {
+    secret: 'keyboard cat',
+    resave: true,    
+    saveUninitialized: true
+}
+
+if (app.get('env') === 'production') {
+    app.set('trust proxy', 1); // trust first proxy
+    sess.cookie.secure = true; // serve secure cookies
+}
+
+app.use(session(sess));
+
 i18n.expressBind(app, {
   locales: ['en', 'es', 'fr', 'de'],
   defaultLocale: 'en',
@@ -27,7 +44,7 @@ i18n.expressBind(app, {
 
 app.use('/dist/leaflet', express.static('node_modules/leaflet/dist'));
 app.use('/dist/feather', express.static('node_modules/feather-icons/dist'));
-
+app.use('/dist/axios', express.static('node_modules/axios/dist'));
 
 app.use(function(req, res, next) {
   req.i18n.setLocaleFromQuery();
@@ -35,7 +52,21 @@ app.use(function(req, res, next) {
   next();
 });
 
+app.use(function(req, res, next) {
+  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
+  res.header(
+    'Access-Control-Allow-Headers',
+    'Origin, X-Requested-With, Content-Type, Accept'
+  );
+  next();
+});
+
+app.use('/api/0', apiRouter);
+
+const csrfProtection = csrf({ cookie: true });
+app.use(csrfProtection);
 app.use('/', indexRouter);
+app.use('/auth', authRouter);
 
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {
diff --git a/bin/www b/bin/www
index 66b60eb..5a5c134 100755
--- a/bin/www
+++ b/bin/www
@@ -4,22 +4,22 @@
  * Module dependencies.
  */
 
-var app = require('../app');
-var debug = require('debug')('soundbirder:server');
-var http = require('http');
+const app = require('../app');
+const debug = require('debug')('soundbirder:server');
+const http = require('http');
 
 /**
  * Get port from environment and store in Express.
  */
 
-var port = normalizePort(process.env.PORT || '3000');
+const port = normalizePort(process.env.PORT || '3000');
 app.set('port', port);
 
 /**
  * Create HTTP server.
  */
 
-var server = http.createServer(app);
+const server = http.createServer(app);
 
 /**
  * Listen on provided port, on all network interfaces.
@@ -34,7 +34,7 @@ server.on('listening', onListening);
  */
 
 function normalizePort(val) {
-  var port = parseInt(val, 10);
+  const port = parseInt(val, 10);
 
   if (isNaN(port)) {
     // named pipe
@@ -58,7 +58,7 @@ function onError(error) {
     throw error;
   }
 
-  var bind = typeof port === 'string'
+  const bind = typeof port === 'string'
     ? 'Pipe ' + port
     : 'Port ' + port;
 
@@ -82,8 +82,8 @@ function onError(error) {
  */
 
 function onListening() {
-  var addr = server.address();
-  var bind = typeof addr === 'string'
+  const addr = server.address();
+  const bind = typeof addr === 'string'
     ? 'pipe ' + addr
     : 'port ' + addr.port;
   debug('Listening on ' + bind);
diff --git a/controllers/api.js b/controllers/api.js
index 8b35959..24a94c7 100644
--- a/controllers/api.js
+++ b/controllers/api.js
@@ -1,3 +1,156 @@
+const debug = require('debug')('soundbirder:api');
+const debugResponses = require('debug')('soundbirder:api:responses');
+require('dotenv').config();
+const axios = require('axios');
+const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY);
+const XenoCanto = require('@unclesamulus/xeno-canto-api');
+const { choices, choice } = require('../utils/choices');
 
-function getRecord(req, res) {
+const OPENCAGE_API_KEY = process.env.OPENCAGE_API_KEY;
+const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json?q=<lat>+<lon>&key=<key>';
+const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5;
+
+function check(req, res) {
+
+}
+
+function quizz(req, res) {
+    debug('Generating quizz');
+    const { lat, lng } = req.body;
+    generateQuizz({ lat, lng }, req.locale)
+        .then(({ species, correct, audio }) => {
+            req.session.correct = correct;
+            res.json({ species, audio });
+            debug("Quizz sent");
+        })
+        .catch(error => {
+            debug("Faced error while generating quizz");
+            res.json({ error });
+            throw error;
+        });
+}
+
+async function generateQuizz(coordinates, locale) {
+    const quizz = {}
+    try {
+        const speciesSelection = await getSpeciesSelection(coordinates);
+        const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
+        quizz.species = await speciesSelectionLocalized;
+        debug("Got species selection", quizz.species);
+        const answer = await choice(speciesSelectionLocalized);
+        debug("Got answer", answer);
+        quizz.correct = answer.code;
+        quizz.audio = await getAudio(answer.sciName);
+    } catch (error) {
+        debug("Error raised while generating quizz");
+        console.error(error);
+    }
+    return quizz;
+}
+
+async function getSpeciesSelection(coordinates) {
+    const { lat, lng } = coordinates;
+    const region = await getRegion(lat, lng);
+    region.country_code = region.country_code.toUpperCase();
+    // const regionCode = await getRegionCode(region);
+    const regionCode = region.country_code;
+    const speciesList = await getSpeciesList(regionCode);
+    const speciesSelection = choices(speciesList, QUIZZ_SIZE);
+    debug("Species proposals:", speciesSelection)
+    return speciesSelection;
+}
+
+function getHome(req, res) {
+    res.render('api', {
+        title: "SoundBirder api",
+        version: 0
+    }
+    );
+}
+
+function getRegion(lat, lon) {
+    const url = OPENCAGE_API_URL
+        .replace('<lat>', lat)
+        .replace('<lon>', lon)
+        .replace('<key>', OPENCAGE_API_KEY);
+    return axios.get(url).then(response => {
+        const { results } = response.data;
+        const { country_code, country, state, county } = results[0].components;
+        return { country_code, country, state, county };
+    }).catch(error => {
+        throw error;
+    });
+}
+
+function getRegionCode({ country_code, country, state, county }) {
+    return eBird.ref.region.list('subnational1', country_code)()
+        .then(states => {
+            console.log(states);
+            const regionCode = states.find(region => region.name === state).code;
+            return regionCode;
+        }).catch(error => {
+            throw error;
+        });
+}
+
+async function getLocalizedNames(speciesCodes, locale) {
+    let names = [];
+    for (let i = 0; i < speciesCodes.length; i++) {
+        const code = speciesCodes[i];
+        const { sciName, comName } = await getLocalizedName(code, locale);
+        names.push({ code, sciName, comName });
+    }
+    return names;
+}
+
+function getLocalizedName(speciesCode, locale) {
+    return eBird.ref.taxonomy.ebird({
+        fmt: 'json',
+        locale: locale,
+        species: speciesCode
+    }).then(
+        response => {
+            const names = response[0];
+            debug("Got localized species names");
+            debugResponses(names);
+            return names;
+        }
+    ).catch(error => {
+        throw error;
+    });
+}
+
+function getSpeciesList(regionCode) {
+    return eBird.product.spplist.in(regionCode)()
+        .then(species => {
+            return species;
+        })
+        .catch(error => {
+            throw error;
+        });
+}
+
+function getAudio(speciesScientificName) {
+    return XenoCanto.search({
+        name: speciesScientificName,
+        quality: 'A'
+    }).then(response => {
+        debugResponses(response);
+        const { recordings } = response;
+        const randomRecord = choice(recordings);
+        const audio = randomRecord.file;
+        return audio;
+    }).catch(error => {
+        throw error;
+    });
+}
+
+const game = {
+    check,
+    quizz
+}
+
+module.exports = {
+    getHome,
+    game
 }
\ No newline at end of file
diff --git a/controllers/auth.js b/controllers/auth.js
new file mode 100644
index 0000000..6a51805
--- /dev/null
+++ b/controllers/auth.js
@@ -0,0 +1,48 @@
+/**
+ * Auth controller 
+ */
+
+function indexPage(req, res) {
+    res.redirect('/auth/login');
+}
+
+function loginPage(req, res) {
+    res.render('auth/login');
+}
+
+function login(req, res) {
+
+}
+
+function logout(req, res) {
+
+}
+
+function registerPage(req, res) {
+    res.render('auth/register');
+}
+
+function register(req, res) {
+
+}
+
+function forgotPassword(req, res) {
+
+}
+
+function resetPassword(req, res) {
+
+}
+
+const auth = {
+    indexPage,
+    login,
+    loginPage,
+    logout,
+    register,
+    registerPage,
+    forgotPassword,
+    resetPassword
+}
+
+module.exports = auth;
\ No newline at end of file
diff --git a/controllers/home.js b/controllers/index.js
similarity index 62%
rename from controllers/home.js
rename to controllers/index.js
index 53a7ca8..b085744 100644
--- a/controllers/home.js
+++ b/controllers/index.js
@@ -1,6 +1,9 @@
 
 function getIndex(req, res) {
-    res.render('index', { title: 'SoundBirder' });
+    res.render('index', {
+        title: 'SoundBirder',
+        csrf_token: req.csrfToken()
+    });
 }
 
 function getAbout(req, res) {
diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/locales/en.js b/locales/en.js
index 32ff642..3cbabc1 100644
--- a/locales/en.js
+++ b/locales/en.js
@@ -7,5 +7,6 @@
 	"Author": "Author",
 	"The project is made with &hearts; by Samuel ORTION": "The project is made with &hearts; by Samuel ORTION",
 	"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.": "SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.",
-	"The project is made with ♥ by Samuel ORTION.": "The project is made with ♥ by Samuel ORTION."
+	"The project is made with ♥ by Samuel ORTION.": "The project is made with ♥ by Samuel ORTION.",
+	"Welcome to SoundBirder' API": "Welcome to SoundBirder' API"
 }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 55c4921..e7a0fa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,14 +8,20 @@
       "name": "soundbirder",
       "version": "0.0.0",
       "dependencies": {
+        "@unclesamulus/ebird-api": "^0.0.0",
+        "@unclesamulus/xeno-canto-api": "^0.0.0",
         "axios": "^0.27.2",
         "cookie-parser": "~1.4.4",
+        "csurf": "^1.11.0",
         "debug": "~2.6.9",
+        "dotenv": "^16.0.1",
         "express": "~4.16.1",
+        "express-session": "^1.17.3",
         "feather-icons": "^4.29.0",
         "http-errors": "~1.6.3",
         "i18n-2": "^0.7.3",
         "leaflet": "^1.8.0",
+        "lodash": "^4.17.21",
         "morgan": "~1.9.1",
         "pug": "^3.0.2"
       }
@@ -37,9 +43,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.18.11",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
-      "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
+      "version": "7.18.13",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
+      "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -48,9 +54,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.18.10",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
-      "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
+      "version": "7.18.13",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
+      "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
       "dependencies": {
         "@babel/helper-string-parser": "^7.18.10",
         "@babel/helper-validator-identifier": "^7.18.6",
@@ -60,6 +66,22 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@unclesamulus/ebird-api": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
+      "integrity": "sha512-cQca/wS+35LuuLoVVB3jDeD3czVBy1FNQBO7uubsnwH9gYZzQEr+TpCeNcMO0S9/ZIjl36fJ3qyFH6R/AsHMFA==",
+      "dependencies": {
+        "axios": "^0.27.2"
+      }
+    },
+    "node_modules/@unclesamulus/xeno-canto-api": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/@unclesamulus/xeno-canto-api/-/xeno-canto-api-0.0.0.tgz",
+      "integrity": "sha512-dVMpX6pbhQvxyjR4eyIMAvR9VNJ/qXnoPc+UQRnA5ESz2JcqVGxcFCByhu88Ym41/QzlfpFwO+/glsGHtzfZWw==",
+      "dependencies": {
+        "axios": "^0.27.2"
+      }
+    },
     "node_modules/accepts": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -249,15 +271,83 @@
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
     "node_modules/core-js": {
-      "version": "3.24.1",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
-      "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==",
+      "version": "3.25.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.0.tgz",
+      "integrity": "sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/core-js"
       }
     },
+    "node_modules/csrf": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
+      "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
+      "dependencies": {
+        "rndm": "1.2.0",
+        "tsscmp": "1.0.6",
+        "uid-safe": "2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/csurf": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
+      "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
+      "dependencies": {
+        "cookie": "0.4.0",
+        "cookie-signature": "1.0.6",
+        "csrf": "3.1.0",
+        "http-errors": "~1.7.3"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/csurf/node_modules/cookie": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+      "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/csurf/node_modules/http-errors": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
+      "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.1.1",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/csurf/node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/csurf/node_modules/setprototypeof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+      "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+    },
+    "node_modules/csurf/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -292,6 +382,14 @@
       "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
       "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
     },
+    "node_modules/dotenv": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
+      "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -358,6 +456,59 @@
         "node": ">= 0.10.0"
       }
     },
+    "node_modules/express-session": {
+      "version": "1.17.3",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
+      "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
+      "dependencies": {
+        "cookie": "0.4.2",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+      "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-session/node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/express-session/node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/express/node_modules/cookie": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
@@ -616,6 +767,11 @@
       "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
       "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
     "node_modules/media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -877,6 +1033,14 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -915,6 +1079,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/rndm": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
+      "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="
+    },
     "node_modules/safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -999,11 +1168,27 @@
         "node": ">=4"
       }
     },
+    "node_modules/toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
     "node_modules/token-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
       "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
     },
+    "node_modules/tsscmp": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+      "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+      "engines": {
+        "node": ">=0.6.x"
+      }
+    },
     "node_modules/type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1016,6 +1201,17 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1075,20 +1271,36 @@
       "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
     },
     "@babel/parser": {
-      "version": "7.18.11",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
-      "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ=="
+      "version": "7.18.13",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
+      "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg=="
     },
     "@babel/types": {
-      "version": "7.18.10",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
-      "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
+      "version": "7.18.13",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
+      "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
       "requires": {
         "@babel/helper-string-parser": "^7.18.10",
         "@babel/helper-validator-identifier": "^7.18.6",
         "to-fast-properties": "^2.0.0"
       }
     },
+    "@unclesamulus/ebird-api": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
+      "integrity": "sha512-cQca/wS+35LuuLoVVB3jDeD3czVBy1FNQBO7uubsnwH9gYZzQEr+TpCeNcMO0S9/ZIjl36fJ3qyFH6R/AsHMFA==",
+      "requires": {
+        "axios": "^0.27.2"
+      }
+    },
+    "@unclesamulus/xeno-canto-api": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/@unclesamulus/xeno-canto-api/-/xeno-canto-api-0.0.0.tgz",
+      "integrity": "sha512-dVMpX6pbhQvxyjR4eyIMAvR9VNJ/qXnoPc+UQRnA5ESz2JcqVGxcFCByhu88Ym41/QzlfpFwO+/glsGHtzfZWw==",
+      "requires": {
+        "axios": "^0.27.2"
+      }
+    },
     "accepts": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -1239,9 +1451,64 @@
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
     "core-js": {
-      "version": "3.24.1",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
-      "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
+      "version": "3.25.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.0.tgz",
+      "integrity": "sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA=="
+    },
+    "csrf": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
+      "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
+      "requires": {
+        "rndm": "1.2.0",
+        "tsscmp": "1.0.6",
+        "uid-safe": "2.1.5"
+      }
+    },
+    "csurf": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
+      "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
+      "requires": {
+        "cookie": "0.4.0",
+        "cookie-signature": "1.0.6",
+        "csrf": "3.1.0",
+        "http-errors": "~1.7.3"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.0",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+          "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+        },
+        "http-errors": {
+          "version": "1.7.3",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
+          "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.4",
+            "setprototypeof": "1.1.1",
+            "statuses": ">= 1.5.0 < 2",
+            "toidentifier": "1.0.0"
+          }
+        },
+        "inherits": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+          "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+        },
+        "setprototypeof": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+          "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+        },
+        "statuses": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+          "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
+        }
+      }
     },
     "debug": {
       "version": "2.6.9",
@@ -1271,6 +1538,11 @@
       "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
       "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
     },
+    "dotenv": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
+      "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ=="
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1335,6 +1607,38 @@
         }
       }
     },
+    "express-session": {
+      "version": "1.17.3",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
+      "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
+      "requires": {
+        "cookie": "0.4.2",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+          "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+        },
+        "depd": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+          "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+        },
+        "safe-buffer": {
+          "version": "5.2.1",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+          "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+        }
+      }
+    },
     "feather-icons": {
       "version": "4.29.0",
       "resolved": "https://registry.npmjs.org/feather-icons/-/feather-icons-4.29.0.tgz",
@@ -1522,6 +1826,11 @@
       "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.8.0.tgz",
       "integrity": "sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA=="
     },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -1744,6 +2053,11 @@
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
     },
+    "random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
+    },
     "range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1770,6 +2084,11 @@
         "supports-preserve-symlinks-flag": "^1.0.0"
       }
     },
+    "rndm": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
+      "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="
+    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1836,11 +2155,21 @@
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
       "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
     },
+    "toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+    },
     "token-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
       "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
     },
+    "tsscmp": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+      "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
+    },
     "type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1850,6 +2179,14 @@
         "mime-types": "~2.1.24"
       }
     },
+    "uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "requires": {
+        "random-bytes": "~1.0.0"
+      }
+    },
     "unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index 049db1c..2a91ef1 100644
--- a/package.json
+++ b/package.json
@@ -6,14 +6,20 @@
     "start": "node ./bin/www"
   },
   "dependencies": {
+    "@unclesamulus/ebird-api": "^0.0.0",
+    "@unclesamulus/xeno-canto-api": "^0.0.0",
     "axios": "^0.27.2",
     "cookie-parser": "~1.4.4",
+    "csurf": "^1.11.0",
     "debug": "~2.6.9",
+    "dotenv": "^16.0.1",
     "express": "~4.16.1",
+    "express-session": "^1.17.3",
     "feather-icons": "^4.29.0",
     "http-errors": "~1.6.3",
     "i18n-2": "^0.7.3",
     "leaflet": "^1.8.0",
+    "lodash": "^4.17.21",
     "morgan": "~1.9.1",
     "pug": "^3.0.2"
   }
diff --git a/public/javascripts/api-client.js b/public/javascripts/api-client.js
new file mode 100644
index 0000000..84e4300
--- /dev/null
+++ b/public/javascripts/api-client.js
@@ -0,0 +1,49 @@
+
+const API_VERSION = "0";
+const API_URL = `/api/${API_VERSION}`;
+
+const TOKEN = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
+
+async function get(url, params) {
+    return await axios.get(url, {
+        params: params,
+        headers: {
+            'X-CSRF-Token': TOKEN
+        }
+    }).then(response => {
+        return response.data;
+    }).catch(error => {
+        throw error;
+    });
+}
+
+async function query(endpoint, params) {
+    return await get(`${API_URL}/${endpoint}`, params);
+}
+
+function getQuizz(lat, lng) {
+    return query('game/quizz', { lat, lng })
+        .then(data => {
+            console.log(data);
+            return data;
+        }).catch(error => {
+            throw error;
+        });
+}
+
+function checkResponse(speciesId) {
+    return query('game/check', {
+        species: speciesId
+    }).then(response => {
+        return response.data.correct
+    }).catch(error => {
+        throw error;
+    });
+}
+
+const client = {
+    getQuizz,
+    checkResponse
+}
+
+export default client;
\ No newline at end of file
diff --git a/public/javascripts/app.js b/public/javascripts/app.js
index 8368e80..a85f5e6 100644
--- a/public/javascripts/app.js
+++ b/public/javascripts/app.js
@@ -1,13 +1 @@
-
-import { geoLocationHandler } from './map.js';
-import './game.js';
-
-feather.replace();
-
-(function () {
-    if (document.getElementById('map') != undefined)
-        geoLocationHandler();
-    let start_button = document.querySelector('.game .start-button');
-    if (start_button != undefined)
-        start_button.addEventListener('click', launchGame);
-}());
+feather.replace();
\ No newline at end of file
diff --git a/public/javascripts/game.js b/public/javascripts/game.js
index 2510ac9..c889f26 100644
--- a/public/javascripts/game.js
+++ b/public/javascripts/game.js
@@ -1,15 +1,52 @@
-const API_VERSION = "1";
+import { geolocationHandler, getCoordinates } from './map.js';
+import client from './api-client.js';
 
-function launch() {
-    
+const API_VERSION = "0";
+
+function geolocationStep() {
+    if (document.getElementById('map') != undefined)
+        geolocationHandler();
+    let start_button = document.querySelector('.game .start-button');
+    if (start_button != undefined)
+        start_button.addEventListener('click', quizzStep);
 }
 
-function get_new_record() {
-    const endpoint = `/api/${API_VERSION}/record`;
+function quizzStep() {
+    // Start by disallowing geolocation step
+    document.querySelector('.game-map-step').classList.toggle('none');
+    // Then allow the quizz step
+    document.querySelector('.game-quizz-step').classList.remove('none');
+
+    // Retrieve coordinates from former done geolocation (TODO: fix the need of cookie)
+    const coordinates = getCoordinates();
+    client.getQuizz(coordinates)
+        .then(quizz => {
+            // Display the quizz
+            displayQuizz(quizz);
+        }).catch(error => {
+            console.log(error);
+        });
 }
 
-const Game = {
-    launch
+function displayQuizz(quizz) {
+    let audio = document.querySelector('.game-quizz-step audio');
+    audio.src = quizz.audio;
+    audio.play();
+    let proposals = document.querySelector('.game-quizz-step .proposals');
+    quizz.species.forEach(sp => {
+        let proposal = document.createElement('li');
+        proposal.classList.add('proposal');
+        let button = document.createElement('button');
+        button.classList.add('proposal-button');
+        button.value = sp.code;
+        button.innerText = sp.comName;
+        proposal.appendChild(button);
+        proposals.appendChild(proposal);
+    });
 }
 
-export default Game;
\ No newline at end of file
+function game() {
+    geolocationStep();
+}
+
+game();
\ No newline at end of file
diff --git a/public/javascripts/map.js b/public/javascripts/map.js
index 44888a9..1dc791d 100644
--- a/public/javascripts/map.js
+++ b/public/javascripts/map.js
@@ -1,6 +1,6 @@
 import { getCookie, setCookie } from './utils.js'
 
-function geoLocationHandler() {
+function getCoordinates() {
     let location = [51.505, -0.09]; // London by default on leaflet
     let lat = getCookie("lat");
     let lng = getCookie("lng");
@@ -8,15 +8,25 @@ function geoLocationHandler() {
         location = [lat, lng];
         console.log(`Got a previous geolocation cookie at ${location[0]}, ${location[1]}`)
     }
+    return location;
+}
+
+function updateLocationCookies([lat, lng]) {
+    setCookie("lat", lat, 10);
+    setCookie("lng", lng, 10);
+}
+
+function geolocationHandler() {
+
+    let location = getCoordinates();
     let message = document.querySelector('.message');
     // Init map
     let map = L.map('map').setView(location, 15);
     L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
         attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
     }).addTo(map);
-    // Init marker
-    let marker = L.marker(location).addTo(map);
 
+    let marker = undefined;
     // Get geolocation
     function getLocation() {
         if (navigator.geolocation) {
@@ -28,17 +38,41 @@ function geoLocationHandler() {
 
     function setLocation(position) {
         location = [position.coords.latitude, position.coords.longitude];
-        marker.setLatLng(location);
+        if (marker === undefined) {
+            marker = new L.marker(location, { draggable: 'true' });
+            marker.on('dragend', updateMarker);
+            marker.addTo(map);
+        } else {
+            marker.setLatLng(location);
+        }
         map.setView(location, 15);
-        setCookie("lat", position.coords.latitude, 10);
-        setCookie("lng", position.coords.longitude, 10);
-        console.log("Geolocation cookie saved for future games");
+        updateLocationCookies(location);
     }
 
+    function updateMap(event) {
+        if (marker === undefined) {
+            marker = new L.marker(event.latlng, { draggable: 'true' });
+            marker.on('dragend', updateMarker);
+            marker.addTo(map);
+        } else {
+            marker.setLatLng(event.latlng);
+        }
+        updateLocationCookies([event.latlng.lat, event.latlng.lng]);
+    }
+
+    function updateMarker(event) {
+        let position = marker.getLatLng();
+        map.panTo([position.lat, position.lng]);
+        updateLocationCookies([position.lat, position.lng]);
+    }
+
+    map.on('click', updateMap);
+
     document.querySelector('.geolocation-button')
         .addEventListener('click', getLocation);
 }
 
 export {
-    geoLocationHandler
+    geolocationHandler,
+    getCoordinates
 }
\ No newline at end of file
diff --git a/public/javascripts/utils.js b/public/javascripts/utils.js
index faf1f8a..82da365 100644
--- a/public/javascripts/utils.js
+++ b/public/javascripts/utils.js
@@ -2,7 +2,7 @@ function setCookie(cname, cvalue, exdays) {
     const d = new Date();
     d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
     let expires = "expires=" + d.toUTCString();
-    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
+    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/" + ";sameSite=strict;";
 }
 
 function getCookie(cname) {
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
index ef198aa..101ad2b 100644
--- a/public/stylesheets/style.css
+++ b/public/stylesheets/style.css
@@ -69,3 +69,7 @@ nav ul {
     flex-direction: row;
     justify-content: space-evenly;
 }
+
+.none {
+    display: none;
+}
diff --git a/routes/api.js b/routes/api.js
new file mode 100644
index 0000000..25e6591
--- /dev/null
+++ b/routes/api.js
@@ -0,0 +1,14 @@
+const express = require('express');
+const router = express.Router();
+const apiController = require('../controllers/api.js');
+
+router.route('/')
+    .get(apiController.getHome);
+
+router.route('/game/quizz')
+    .get(apiController.game.quizz);
+
+router.route('/game/check')
+    .post(apiController.game.check);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/auth.js b/routes/auth.js
new file mode 100644
index 0000000..54e1594
--- /dev/null
+++ b/routes/auth.js
@@ -0,0 +1,11 @@
+const express = require('express');
+const router = express.Router();
+const authController = require('../controllers/auth');
+
+router.route('/')
+    .get(authController.indexPage);
+    
+router.route('/login')
+    .get(authController.loginPage);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 1d79505..57f443c 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,12 +1,11 @@
-var express = require('express');
-var router = express.Router();
-var homeController = require('../controllers/home');
-/* GET home page. */
+const express = require('express');
+const router = express.Router();
+const indexController = require('../controllers/index');
+
 router.route('/')
-        .get(homeController.getIndex);
+        .get(indexController.getIndex);
 
 router.route('/about')
-        .get(homeController.getAbout);
-
+        .get(indexController.getAbout);
 
 module.exports = router;
diff --git a/utils/choices.js b/utils/choices.js
new file mode 100644
index 0000000..6fc2afc
--- /dev/null
+++ b/utils/choices.js
@@ -0,0 +1,34 @@
+/**
+ * Get a random selection from an array.
+ * 
+ * @param {Array} array
+ * @param {Number} number 
+ * @returns `number` random elements from the array
+ */
+function choices(array, number) {
+    const result = [];
+    for (let i = 0; i < number; i++) {
+        let c = choice(array);
+        if (result.includes(c)) {
+            i--;
+        } else {
+            result.push(choice(array));
+        }
+    }
+    return result;
+}
+
+/**
+ * Get a random element from an array.
+ * 
+ * @param {Array} array 
+ * @returns a random element from the array
+ */
+function choice(array) {
+    return array[Math.floor(Math.random() * array.length)];
+}
+
+module.exports = {
+    choices,
+    choice
+}
\ No newline at end of file
diff --git a/views/about.pug b/views/about.pug
index b15f494..a8b9b83 100644
--- a/views/about.pug
+++ b/views/about.pug
@@ -1,8 +1,8 @@
 extends layout.pug 
 
 block content 
-  p #{ __("SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.") }
+    p #{ __("SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.") }
+
+    h2 #{ __("Author") }
+    p #{ __("The project is made with ♥ by Samuel ORTION.") }
 
-  h2 #{ __("Author") }
-  p #{ __("The project is made with ♥ by Samuel ORTION.") }
-  
diff --git a/views/api.pug b/views/api.pug
new file mode 100644
index 0000000..e92e4a2
--- /dev/null
+++ b/views/api.pug
@@ -0,0 +1,5 @@
+extends layout
+
+block content 
+    h2= __('Welcome to SoundBirder\'s API')
+    .version api v#{version} 
\ No newline at end of file
diff --git a/views/error.pug b/views/error.pug
index 51ec12c..3b25cfa 100644
--- a/views/error.pug
+++ b/views/error.pug
@@ -1,6 +1,6 @@
 extends layout
 
 block content
-  h1= message
-  h2= error.status
-  pre #{error.stack}
+    h1= message
+    h2= error.status
+    pre #{error.stack}
diff --git a/views/game.pug b/views/game.pug
index 39053f1..ea63aa8 100644
--- a/views/game.pug
+++ b/views/game.pug
@@ -1,8 +1,18 @@
 .game
-  #map 
-  button.button.geolocation-button  
-    i(data-feather="map-pin")
-  button.button.start-button
-    i(data-feather="play")
-  link(rel="stylesheet", href="/dist/leaflet/leaflet.css")
-  script(src="/dist/leaflet/leaflet.js") 
\ No newline at end of file
+    .game-map-step
+        #map 
+        button.button.geolocation-button  
+            i(data-feather="map-pin")
+        button.button.start-button
+            i(data-feather="play")
+    .game-quizz-step.none
+        ul.proposals 
+        audio
+    .game-result-step.none
+        p.result 
+        button.button.restart-button.disabled
+            i(data-feather="repeat")
+    link(rel="stylesheet" href="/dist/leaflet/leaflet.css")
+    script(src="/dist/leaflet/leaflet.js")
+    script(src="/dist/axios/axios.min.js")
+    script(src="/javascripts/game.js" type="module") 
\ No newline at end of file
diff --git a/views/index.pug b/views/index.pug
index 7cfe919..185c3f0 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -1,4 +1,4 @@
 extends layout
 
 block content
-  include game     
+    include game     
diff --git a/views/layout.pug b/views/layout.pug
index 3a821ef..70b1065 100644
--- a/views/layout.pug
+++ b/views/layout.pug
@@ -1,26 +1,26 @@
 doctype html
 html
-  head
-    title= title
-    meta(name="iewport", content="width=device-width, initial-scale=1.0")
-    link(rel="stylesheet", href="/stylesheets/style.css")
-  body
-    header 
-      h1= title
-    nav
-      ul
-        li
-          a(href="/") #{ __("Game") }
-        li
-          a(href="/about") #{ __("About") }
-        li
-          a(href="/contact") #{ __("Contact") }
-    main 
-      block content
-    footer 
-      .description 
-      .copyright Copyright &copy; 2022 - 
-        span.author 
-          a(href="https://samuel.ortion.fr", class="link") Samuel ORTION
-    script(src="/javascripts/app.js" type="module")
-    script(src="/dist/feather/feather.min.js") 
\ No newline at end of file
+    head
+        title= title
+        meta(name="viewport" content="width=device-width, initial-scale=1.0")
+        link(rel="stylesheet" href="/stylesheets/style.css")
+        if csrf_token 
+            meta(name="csrf-token" content=csrf_token)
+    body
+        header 
+            h1= title
+        nav
+            ul
+                li
+                    a(href="/") #{ __("Game") }
+                li
+                    a(href="/about") #{ __("About") }
+        main 
+            block content
+        footer 
+            .description 
+            .copyright Copyright &copy; 2022 - 
+                span.author 
+                    a(href="https://samuel.ortion.fr" class="link") Samuel ORTION
+        script(src="/javascripts/app.js" type="module")
+        script(src="/dist/feather/feather.min.js") 
\ No newline at end of file