Criar uma app REST API com CRUD + Node.js + MongoDB + EJS

Nuno Filipe
45 min readApr 7, 2019

--

Este é um projeto de aprendizagem que os autores desejam publicamente partilhar. Iremos criar uma REST API com CRUD, Node.js e MongoDB. O front-end da aplicação será criado com EJS. Na reta final do projeto a app será alojada num serviço da cloud (HEROKU).

A intenção é criar uma aplicação que permita aos utilizadores inserirem pontos de interesse genéricos na base de dados (mongoDB), a base de dados crescerá com a inserção de registos de vários utilizadores em vários locais geográficos.

Com um simples click a app devolverá os 10 pontos de interesse mais próximos de si num raio de 300 km.

Para guardar a nossa localização (as coordenadas geográficas do ponto de interesse que queremos inserir na base de dados), faremos uso da API navigator-geolocation, para renderizar os mapas de cada ponto de interesse da B.D. usaremos a API static-maps da Google.

O que é uma REST API ?

Muito suscintamente uma REST API é uma API que permite a interoperabilidade entre sistemas usando JSON ou XML e os métodos GET, POST, PUT, DELETE do protocolo HTTP.

REST API

Requisitos para criação do projeto

No momento para o desenvolvimento deste projeto é necessário ter instalado o Node.js. Mais tarde será necessário criar uma base de dados mongoDB no mLab, tal será assinalado oportunamente.

Vamos começar …

. Iniciar projeto

Criar uma pasta de nome ‘PontosInteresse_App’.

Abrir a linha de comando na posição onde se encontra a pasta criada e executar o comando:

npm init -y
npm init

O comando criará o ficheiro ‘package.json’. Este ficheiro guarda informações sobre o projeto e também sobre as dependências (pacotes) do mesmo.

. Instalação de pacotes necessários

Para este projeto vamos precisar de instalar os módulos:

  • express.js uma framework para Node que permite o desenvolvimento de aplicações web de uma forma muito simples — Saber mais aqui
  • body-parser — pacote usado para manipular solicitações JSON.
  • mongoose — biblioteca NodeJs para inter-agir com a B.D. MongoDb
  • ejs — template engine para criar o front-end da app

Para instalar estes pacotes usaremos o comando:

npm install --save express body-parser mongoose ejs
fig.1 — ver dependências no ficheiro package.json

. Criar servidor com Node.js

Dentro da pasta ‘PontosInteresse_App’ criar o ficheiro ‘app.js’ (como indicado na fig.1)

// associar as dependências instaladas
const express = require('express');
// inicializar app express
const app = express();
let port = 5000;// servidor á escuta no porto 5000
// 'process.env.port': caso usemos Heroku
app.listen(process.env.port || port, () =>{
console.log('Servidor em execução no porto: '+ port);
});
fig.2 — servidor

Organização da Aplicação (MVC)

A nossa aplicação deverá seguir o “designMCV. M de Model (parte do código que representa os dados da aplicação), V de View (layouts) e C de Controllers (parte lógica da app a forma como a app lida com as solicitações recebidas e as respostas enviadas).

Existem também as Routes que indicam ao cliente (browser ou app mobile) qual o controller a usar, de acordo com o url/path solicitado.

. Dentro da nossa pasta ‘PontosInteresse_App’ vamos então criar a seguinte estrutura de pastas e ficheiros:

  • \controllers\apiController.js
  • \models\PImodel.js
  • \routes\api.js
  • \views\PItemplate.ejs
fig.3 — MVC + R

. Criar um end-point de teste usando controlador ‘apiController.js’, rota ‘api.js’ e aplicação principal ‘app.js’

No ficheiro ‘\controllers\apiControllers.js’ colocar o seguinte código:

exports.test = function (req, res) {
res.send(‘Olá! Teste ao Controller’);
};

No ficheiro ‘\routes\api.js’ colocar o seguinte código:

const express = require (‘express’);
const router = express.Router();
// importa controlador 'apiController.js' da pasta:
// ‘../controllers/apiController’
const apiController = require(‘../controllers/apiController’);
// url do teste será: http://localhost:5000/api/teste
router.get(‘/teste’, apiController.test);
module.exports = router;

No ficheiro ‘app.js’ acrescentar o seguinte código:

...
// ‘END POINT INVÁLIDO!’
app.get(‘/’, function(req, res){
res.send(‘END POINT INVÁLIDO!’);
});
// todo o url começado por ‘/api’ chama as rotas em ‘./routes/api’
const routes = require(‘./routes/api’);
app.use(‘/api’, routes);
...
fig.4 — app.js

Testar end-point no navegador:

fig.5 — teste no navegador

. Criar o protótipo das rotas e controladores da aplicação

No ficheiro ‘\controllers\apiControllers.js’ acrescentar o seguinte código:

...
// TODO: listar pontos de interesse da BD
exports.details = function (req, res) {
res.send({type: ‘GET’});
};
// TODO: adicionar novo ponto de interesse
exports.add = function (req, res) {
res.send({type: ‘POST’});
};
// TODO: atualizar ponto de interesse
exports.update = function (req, res) {
res.send({type: ‘PUT’});
};
// TODO: apagar ponto de interesse
exports.delete = function (req, res) {
res.send({type: ‘DELETE’});
};
fig.6 — apiControllers.js

No ficheiro ‘\routes\api.js’ colocar o seguinte código:

...
// url do teste será: http://localhost:5000/api/teste
router.get(‘/teste’, apiController.test);
// TODO: listar pontos de interesse da BD
router.get(‘/details’,apiController.details);
// TODO: adicionar novo ponto de interesse
router.post(‘/interest’,apiController.add);
// TODO: atualizar ponto de interesse
router.put(‘/interest/:id’,apiController.update);
// TODO: apagar ponto de interesse
router.delete(‘/interest/:id’,apiController.delete);
module.exports = router;
fig.6 — api.js

Testar a rota ‘/api/details’ no navegador

fig.7 — teste no navegador

. Testar a API no Postman

O Postman é uma aplicação que permite realizar testes ás REST-APIs que se encontram na fase de desenvolvimento. O Postman permite fazer requisições HTTP como POST e PUT a partir de uma interface gráfica.

Por se tratar de uma aplicação para o Chrome, o Postman é facilmente instalado a partir da Chrome Web Store, link aqui.

A titulo de exemplo vamos testar o end-point: ‘http://localhost:5000/api/interest/randomId’ com metodo PUT. De notar que os métodos PUT e DELETE têm um query id que aqui foi designado aleatóriamente como ‘randomId’.

fig.8 — postman

Vamos agora tratar do POST request usando um middleware (body-parser) que permita ao servidor na resposta ao pedido devolver os dados da mensagem enviada. Middleware é todo o código disparado entre o pedido e a resposta.

fig.9 — middleware

No ficheiro ‘\controllers\apiControllers.js’ atualizar a função ‘create’:

...
// adicionar novo ponto de interesse
exports.create = function (req, res) {
console.log(‘You made a POST request: ‘, req.body);
res.send({
type: ‘POST’,
name: req.body.name,
rank: req.body.rank });
};

...

No ficheiro ‘app.js’ acrescentar o seguinte código:

...
const bodyParser = require(‘body-parser’);
// este middleware deve estar acima das routes-handlers!
app.use(bodyParser.json());
...

Criar um objeto JSON para passar no campo Body do Postman

{
"name": "nuno",
"rank": "noob"
}
fig.9— postman

Notar que ao clicar ‘Send’ obtenho na resposta o objeto JSON enviado.

. Criar uma base de dados no mLab

Comecemos por instalar o pacote ‘mongoose’ que permite converter os dados da B.D. em objetos JavaScript para que possam ser utilizados na aplicação. Fornece-nos ainda métodos que facilitam a inter-acção com os dados na B.D. e criação de modelos e Schemas.

npm install mongoose -save

É necessário criar uma conta no mLab , nessa conta foi criado um cluster designado de ‘nodeJsCluster ’ uma B.D. de nome ‘test’, uma coleção ‘PontosInteresse ’, e uma password ‘nnn’. No final é necessário copiar a ‘connection string’ fornecida para podermos efetuar a ligação da app com a B.D.

Saber mais aqui.

. Estabelecer a ligação com a B.D.

No ficheiro ‘app.js’ acrescentar o seguinte código:

...
const mongoose = require(‘mongoose’);
// Ligar á B.D.: 'test'->user da BD, ´nnn´->pass
mongoose.connect(‘mongodb+srv://test:nnn@nodejscluster-art2k.mongodb.net/test?retryWrites=true’);
// Confirma ligação na consola
mongoose.connection.on(‘connected’, function () {
console.log(‘Connected to Database ‘+’test’);
});
// Mensagem de Erro
mongoose.connection.on(‘error’, (err) => {
console.log(‘Database error ‘+err);
});
...
fig.10 — app.js

Se corrermos a aplicação com

node app.js

Confirmamos a ligação na consola com a mensagem ‘Connected to Database test

fig.11 — msg consola

. Criar um Modelo para B.D. baseado num Schema

Vamos criar um modelo baseado num Schema que será a representação dos dados a enviar e manipular na B.D.

No ficheiro ‘\models\PImodel.js’ colocar o seguinte código:

const mongoose = require(‘mongoose’);
const Schema = mongoose.Schema;
// PI Schema
const PISchema = new Schema({
name: {
type: String,
required: [true, ‘*Campo obrigatório!’]
},
details: {
type: String
},
status: {
type: Boolean,
default: true
}
// TODO: geo location
});
// criar Modelo_PI baseado em PISchema: ‘PontosInteresse’->nome da // coleção
const PI = mongoose.model(‘PontosInteresse’, PISchema);
// exportar Modelo_PI
module.exports = PI;

Operações do CRUD

Vamos criar as operações CRUD: create, read, update, delete.

CREATE

. Criar um Ponto de Interesse (PI) na base dados

No ficheiro ‘\controllers\apiControllers.js’ atualizar a função ‘create’:

// importar modelo
const PI = require('../models/PImodel');
// adicionar novo ponto de interesse
exports.create = function (req, res) {
// cria novo ‘pi’ na BD a partir do request, depois, devolve o
//‘pi’ criado ao cliente
PI.create(req.body).then(function(pi){
res.send(pi);
});

};

Teste no postman

fig.12 — postman

Confirmar criação de Ponto de Interesse no mongoDB

fig.13 — mongoDB

. Criar middleware para tratar erros ocorridos nas rotas

Se tentarmos inserir um documento na coleção da B.D. cujo campo obrigatório (name) seja nulo o front-end da aplicação fica á espera de uma resposta que nunca chega pois ocorreu um erro na route-handlers.

fig.14 — middleware

Podemos resolver isto da seguinte forma…

No ficheiro ‘\controllers\apiControllers.js’ editar a função ‘create’:

// ocorrido um erro, ‘next’ chama proximo middleware (ver ‘app.js’)
exports.create = function (req, res, next) {
PI.create(req.body).then(function(pi){
res.send(pi);
}).catch(next);
};

No ficheiro ‘app.js’ acrescentar:

// error handling middleware
app.use(function(err, req, res, next){
console.log(err);

// ‘res.status(422)’->muda o status
res.status(422).send({error: err.message});
});

Ter atenção á ordem do middleware!

fig.15–app.js

Teste no postman

fig.16–postman

Notar a mudança do Status para 422 e a mensagem de erro devolvida!

DELETE

. Apagar um Ponto de Interesse (PI)

Para apagar um documento (PI) da coleção temos de passar na rota o ‘id ’que o identifica.

No ficheiro ‘\controllers\apiControllers.js’ editar a função ‘delete’:

// ‘_id:’->nome da propriedade na BD, 
// ‘req.params.id’->devolve-me o parametro id na req
exports.delete = function (req, res, next) {
// apaga ‘pi’ da BD, depois, devolve o ‘pi’ apagado ao cliente
PI.findByIdAndRemove({_id: req.params.id}).then(function(pi){
res.send(pi);
}).catch(next);
};

Se criarmos um ‘PI ’ no postman (ver fig.12) poderemos copiar o ‘_id ’que nos é devolvido na resposta. Se em seguida mudarmos o método de POST para DELETE e passarmos o ‘_id ’ copiado para a rota, fazendo ‘Send’ apagaremos o documento na B.D.

fig.17 — postman

UPDATE

. Atualizar um (PI)

Á semelhança da função ‘delete ’ para atualizar um documento da coleção (PI) temos de passar na rota o ‘id ’que o identifica e no body as propriedades a atualizar.

No ficheiro ‘\controllers\apiControllers.js’ editar a função ‘update’:

// atualiza ‘pi’ da BD com as propriedades em ‘req.body’
// depois, procura de novo na BD o ‘pi’ atualizado (senão manda o pi // não atualizado!)
// depois, devolve o ‘pi’ atualizado ao cliente
exports.update = function (req, res, next) {
IP.findByIdAndUpdate({_id: req.params.id},
req.body).then(function(){
IP.findOne({_id: req.params.id}).then(function(ip){
res.send(ip);
});
}).catch(next);
};

Se criarmos um ‘PI ’ no postman (ver fig.12) poderemos copiar o ‘_id ’que nos é devolvido na resposta. Se em seguida mudarmos o método de POST para PUT e passarmos o ‘_id ’ copiado para a rota, fazendo ‘Send’ atualizaremos o documento na B.D.

fig.18 — postman

READ

. Listar Pontos de Interesse perto da localização do utilizador da app

O mongoDB trata da geolocalização usando GeoJson que é um método de guardar dados geográficos em formato json.

O mongoDB pode usar como geolocalização o método 2D ou 2D Sphere. Imaginem que querem medir a distancia entre 2 pontos diferentes no globo, pontos A e B, o método 2D mede a distancia em linha reta no plano enquanto que o método 2D Sphere toma em conta a curvatura da Terra.

fig.19–2D, 2D SPHERE

Passaremos á atualização do modelo ‘PImodel.js’ para incluir dados de geolocalização e usaremos 2D Sphere.

No ficheiro ‘\models\PImodel.js’ editar o modelo da seguinte forma:

...
// geolocation Schema
const GeoSchema = new Schema({
type: {
type: String,
default: ‘Point’
},
coordinates: {
type: [Number],
index: ‘2dsphere’
}
});
// PI Schema
const PISchema = new Schema({
name: {
type: String,
required: [true, ‘*Campo obrigatório!’]
},
details: {
type: String
},
status: {
type: Boolean,
default: true
},
geometry: GeoSchema
});
...

No ficheiro ‘\controllers\apiControllers.js’ editar a função ‘details’:

// procura todos os doc. na BD, guarda em ‘pi’ 
// depois devolve ‘pi’ ao cliente
exports.details = function (req, res) {
PI.find({}).then(function(pi){
res.send(pi);
});
};

Teste no postman

fig.20 — Postman-listAll

Os ‘PIs’ listados foram introduzidos previamente usando o end-point correspondente (CREATE — ver fig.12).

Notar que aqui a rota utilizada foi: ‘/api/details’.

Conseguimos, no momento, listar todos os documentos da coleção no entanto não é isto que pretendemos.

Queremos passar uma latitude e longitude e depois procurar por PIs que estejam próximos desses parâmetros. Esses parâmetros são passados no browser (cliente) e denominam-se por parâmetros url e não devem ser confundidos com parâmetros de request como o ‘_id’ que passamos no DELETE ou UPDATE.

Parâmetros url começam por ‘?’ e um exemplo pode ser: www.pontosinteresse.com/api/details?lng=56.45&lat=43.37

‘lng=56.45&lat=43.37’ — ‘lng’ e ‘lat’ são os nomes atribuídos ás variáveis. ‘56.45’ e ‘43.37’ são o valor que as variáveis tomam.

Desta forma quando fizermos um GET-REQUEST ao servidor vamos passar um parâmetro de longitude e latitude e a query á B.D. vai ter em conta esses valores e devolver os PIs próximos dessa gama de valores.

A query á B.D. vai ser feita usando ‘$geoNear aggregation stage’. Saber mais aqui.

Passemos ao código…

No ficheiro ‘\controllers\apiControllers.js’ editar a função ‘details’:

// listar PIs baseado na distancia relativa aos valores de lng e lat 
// passados pelo cliente
module.exports.details = (req, res, next) =>{
let lng = parseFloat(req.query.lng);
let lat = parseFloat(req.query.lat);
const maxDist = 100000;
PI.aggregate([{
‘$geoNear’: {
“near”: { ‘type’: ‘Point’,
“coordinates“: [parseFloat(lng), parseFloat(lat)] },
“spherical”: true,
“distanceField”: ‘dist’,
“limit”: 3,
“maxDistance”: maxDist
}
}])
.then(pi => res.send(pi))
.catch(next);
};

Explicação de alguns pontos do código:

  • near’: { ‘type’: ‘Point’, — porque procuramos um ponto no mapa
  • coordinates’: [parseFloat(lng), parseFloat(lat)] } — array de coordenadas cujos valores são recebidos através de req.query.x (‘x’-variável passada na query string), estes valores recebidos são strings mas queremos números pelo que fazemos o parse destes valores.
  • ‘spherical ’:‘true ’— define que a distancia medida deverá ter em conta a curvatura da Terra.
  • distanceField’: ‘dist’, — distancia calculada entre o PI na B.D. e o ponto passado pelo cliente (‘lng’ e ‘lat’).
  • limit’: ‘3 ’— max. nº de documentos a devolver será 3.
  • maxDistance’:‘maxDist’define o raio de pontos a procurar (PIs na base de dados), relativamente ao ponto passado pelo cliente (‘lng’ e ‘lat’). O valor de maxDist’ é em metros!

Teste no Postman

fig.21 — Postman-part.1
fig.22 — Postman-part.2

Introduzida a rota ‘/api/details/’ com os parâmetros ‘?lng=-80 & lat=25’ e feito o GET-REQUEST, obtivemos tres PIs ordenados da distancia menor para a maior em relação á longitude e latitude providenciadas pelo cliente.

CRIAÇÃO DO FRONT-END COM EJS

Vamos usar o template-engine EJS (Embedded Javascript) para criar o nosso front-end, para tal comecemos por configurar isso mesmo no ficheiro ‘app.js’.

// Configuração da view engine
app.set('view engine', 'ejs');

Agora o nosso projeto sabe que vamos usar EJS para os nossos templates. De seguida queremos implementar a possibilidade de servir ficheiros estáticos ao cliente a partir do servidor, para tal vamos acrescentar um novo middleware no ficheiro ‘app.js’ com express e criar as pastas ‘/public/assets’ que receberão os ficheiros estáticos que pretendemos servir.

app.use(express.static(‘public’));
view-engine + staticFiles-middleware

Se pretender por exemplo fazer um GET de um ficheiro ‘styles.css’ cujo caminho esteja referenciado em ‘createPI.ejs’ (‘<link rel=”stylesheet” type=”text/css” href=”/assets/styles.css”>’) a partir da rota ‘/’, o pedido chega ao servidor, encontra este middleware , verifica que é um ficheiro estático e serve esse ficheiro a partir da pasta ‘/public/assets/img.jpg’ .

Vamos então criar o ficheiro ‘createPI.ejs’ dentro da pasta ‘/views’. Este ficheiro receberá o HTML que iremos renderizar e testar na rota ‘/api/teste’.

Atualizar o controlador ‘/controllers/apiController.js’ com o seguinte código:

// exporta function 'test', chamada em '/routes/api.js'
exports.test = function (req, res) {
// res.send(‘Olá! Teste ao Controller’);
res.render(‘PI’);
};
apiController.js

Editar ‘views/createPI.ejs’:

<!DOCTYPE <html>
<html lang=”en”>
<head>
<meta charset=”UTF-8">
<title>TESTE</title>
<link rel=”stylesheet” type=”text/css”
href=”/assets/colocoAquiMeu.css”>
</head>
<body>
<h2>HeaderTitulo<h2>
<form action=”/colocoAquRotaAredireccionarNoSubmit”
method=”POST”>
<input type=”text” placeholder=”placeholder01" name=”name01">
<input type=”text” placeholder=”placeholder02" name=”name02">
<input type=”text” placeholder=”placeholder03" name=”name03">
<button type=”submit”>Submit</button>
</form>
</body>
</html>

Testar no browser com: ‘http://localhost:5000/api/teste’

teste no browser

O Express não consegue interpretar os dados passados a partir do elemento ‘<form>’. O bodyParser-middleware que temos usado até agora serviu-nos para fazer o parse de objetos JSON . Para fazer parse de dados do elemento ‘<form>’ temos de reconfigurar o bodyParser.

Em ‘app.js’ editar:

// antes -> app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

OPERAÇÕES CRUD NO FRONT-END

. CREATE

Vamos criar uma rota raiz ‘/’ em ‘app.js’ e renderizar o template ‘/views/createPI.ejs’.

Em ‘app.js’ adicionar o seguinte código:

// rota raiz: renderiza a pagina onde criamos novo PI
app.get('/', function (req, res) {
res.render('createPI');
});
app.js

Em ‘views/createPI.ejs’ vamos criar os inputs com os campos de ‘PI ’ . A rota ‘/api/interest’ (rota onde é adicionado novo ‘PI ’ na B.D.) é chamada ao clicar ‘Submit’.

Atualizar ‘views/createPI.ejs’:

<!DOCTYPE <html>
<html lang=”en”>
<head>
<meta charset=”UTF-8">
<title>TESTE</title>
<link rel=”stylesheet” type=”text/css”
href=”/assets/colocoAquiMeu.css”>
</head>
<body>
<h2>Criar Ponto Interesse<h2>

<%# "/api/interest"-> rota de criação de novo registo %>
<form action=”/api/interest” method=”POST”>
<input type=”text” placeholder=”name" name=”name">
<input type=”text” placeholder=”details" name=”details">
<input type=”text” placeholder=”longitude" name=”lng">
<input type="text" placeholder="latitude" name="lat">
<button type=”submit”>Submit</button>
</form>
</body>
</html>

Os atributos ‘name’ dos inputs receberão os valores do cliente (browser) e estes serão passados para o body no request.

A função ‘create em ‘apiController.js’ será chamada na rota ‘/api/interest’ ao clicar ‘Submit’. Devemos atualizar esta função de forma a que no request esta recolha os dados enviados no body, crie um novo documento na B.D.(baseado nos dados recolhidos) e redireccione para uma rota que liste os pontos de interesse (PIs).

Em ‘/controllers/apiController.js’ atualizar:

exports.create = function (req, res, next) {
// inicializar variaveis com os valores do 'req.body'
let nm = req.body.name;
let dt = req.body.details;
let lng = req.body.lng;
let lat = req.body.lat;

// criar variavel baseada no modelo 'PImodel' para receber dados
// do formulario (request)
let data = {
name: nm,
details: dt,
status: true,
geometry: {"type": "point", "coordinates": [lng, lat]}
};

// cria novo 'pi' na BD a partir do request, depois, envia msg
// para o cliente (Browser)
PI.create(data).then(function(pi){
res.send('Documento criado com sucesso!');

// res.redirect('/api/listall');
}).catch(next);
};
apiController.js

Para verificar o funcionamento, reiniciamos o servidor e preenchemos o formulário. Após carregar-mos no botão ‘Submit’, deverá aparecer a mensagem de sucesso: ‘‘Documento criado com sucesso!’.

Browser-criar PI
Browser-mensagem

. READ

Vamos criar uma rota onde possamos listar os pontos de interesse (PIs), criados.

Em ‘/routes/api.js’ adicionar o seguinte código:

// listar todos os pontos de interesse da BD
router.get('/listAll',apiController.listAllPIs);

Em ‘/controllers/apiController.js’ adicionar o seguinte código:

// listar todos os pontos de interesse da BD
exports.listAllPIs = function (req, res, next) {
PI.find({}).then(function(pi){
res.render('listPIs', {pis: pi});
}).catch(next);
};

Esta função vai procurar todos os documentos na B.D. e passá-los para o template ‘listPIs.ejs’.

pis’ em ‘{pis: pi}’ é a propriedade que vai receber os documentos e terá de ser posteriormente referenciada no template para a exibição dos dados.

Vamos então criar o template /views/‘listPIs.ejs’’:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lista PIs</title>
</head>
<body>
<h2>Lista de Pontos de Interesse<h2>
<table border="1">
<thead>
<tr>
<td>Name</td>
<td>Details</td>
<td>Status</td>
</tr>
</thead>
<tbody>
<% pis.forEach(function(data) { %>
<tr>
<td><%= data.name %></td>
<td><%= data.details %></td>
<td><%= data.status %></td>
</tr>
<% }) %>
</tbody>
<button><a href="/">Back</a></button>
</body>
</html>

De notar que os dados dos documentos são recebidos e manipulados a partir do objeto ‘pis’, com um ciclo ‘forEach’. Dos dados retiraremos os atributos ‘name’, ‘details’ e ‘status’ (atributos de ‘PI’).

Queremos que, ao criar um novo ponto de interesse sejamos redireccionados para a rota ‘/api/listall’, para tal basta editar a função ‘create’ em ‘/controllers/apiController.js’ :

exports.create = function (req, res, next) {
...
// cria novo 'pi' na BD a partir do request, depois, redirecciona
// para rota '/api/listall'
PI.create(data).then(function(pi){
res.redirect('/api/listall');
}).catch(next);
};
apiController.js

Vamos criar um ponto de interesse a partir do Browser:

. Preenchimento dos campos:

Browser-Criar PI

. Depois de clicar ‘submit’:

Browser — Listar Pontos de Interesse

NOTA: Previamente apagámos todos os documentos da base de dados e de seguida inserimos este, daí o resultado exibido na figura (faremos isto recorrentemente ao longo do trabalho).

. Listar Pontos de Interesse perto da localização do utilizador da app

Em seguida vamos listar apenas os pontos de interesse próximos de uma determinada localização, conseguimos fazer isso na rota ‘/api/details’, queremos agora renderizar essa informação.

Em ‘controllers/apiController.js’ editar a função ‘details’ da seguinte forma:

// listar PIs baseado na distancia relativa aos valores de lng e lat 
// passados pelo cliente
module.exports.details = (req, res, next) =>{
let lng = parseFloat(req.query.lng);
let lat = parseFloat(req.query.lat);
const maxDist = 100000;
PI.aggregate([{
‘$geoNear’: {
“near”: { ‘type’: ‘Point’,
“coordinates“: [parseFloat(lng), parseFloat(lat)] },
“spherical”: true,
“distanceField”: ‘dist’,
“limit”: 3,
“maxDistance”: maxDist
}
}])
.then(pi => res.render('listPIs', {pis: pi}))

.catch(next);
};

Introduzimos mais alguns documentos na B.D… Ao fazer a listagem dos pontos de interesses a partir do Browser encontramos:

Browser — listall

Façamos a listagem dos pontos de interesse próximos da longitude = -80.87 e latitude = 25.80 num raio de 100 Km (valor hardcoded definido na função ‘details’).

Passamos esses parametros a partir de uma query string na rota ‘/api/details/’:

Browser — details

De notar que o “Restaurante Rodizio” não foi listado pois este encontra-se fora do limite (100 km) .

. UPDATE

Antes de passar á operação update, decidimos renomear algumas rotas, criar a rota ‘api/edit/:id’ e atualizar o código em conformidade.

Em ‘\routes\apis’ fizemos:

const express = require ('express');
const router = express.Router();const apiController = require('../controllers/apiController');
// rota de testes
router.get('/teste', apiController.test);
// criar novo ponto de interesse
router.post('/create',apiController.create);
// listar um pontos de interesse por id (para editar)
router.get('/edit/:id',apiController.edit);
// listar todos os pontos de interesse da BD
router.get('/listAll',apiController.listAll);
// detalhes dos pontos de interesse proximos da lng/lat introduzidas
router.get('/details',apiController.details);
// atualizar ponto de interesse
router.post('/update/:id',apiController.update);
// apagar ponto de interesse
router.get('/delete/:id',apiController.delete);
module.exports = router;
api.js

Notar a criação de uma rota ‘api/edit/:id’ que será usada para listar um ponto de interesse baseado num ‘id’ e exibido num template editPI.ejs’ (a criar). Os dados recebidos (num formulário) serão submetidos como ‘POST’ na rota ‘/api/update/’.

Em ‘\controllers\apiControllers.js’ vamos criar a função ‘edit’ e atualizar a função ‘update’:

// listar um ponto de interesse por id (para editar)
exports.edit = function (req, res, next) {
PI.findOne({_id: req.params.id}).then(function(pi){
res.render('editPI', {pi: pi});
}).catch(next);
};
// atualiza 'pi' da BD com os valores do formulário
exports.update = function (req, res, next) {
PI.findByIdAndUpdate({_id: req.params.id},
req.body).then(function(){
res.redirect('/api/listall');
}).catch(next);
};

Em ‘\views’ vamos criar um ficheiro ‘editPI.ejs’ e adicionar seguinte código:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Edit PI</title>
</head>
<body>
<h2>Edit</h2>
<form action="/api/update/<%= pi._id %>" method="POST">
<input type="text" value="<%= pi.name %>" name="name">
<input type="text" value="<%= pi.details %>" name="details">
<input type="text" value="<%= pi.status %>" name="status">
<br />
<button><a href="/api/listall">List All</a></button>
<button type="submit">Edit</button>
</form>
</body>
</html>
editPI.js

Notar que, na submissão do formulário (botão ‘edit’) chama a rota ‘/api/update/’ e os dados do formulário serão passados como ‘POST’ que a função ‘update’, em ‘\controllers\apiControllers.js’, recebe para fazer a atualização do documento na base de dados.

Vamos atualizar o template ‘listPIs.ejs’ de forma a que a partir deste, possamos fazer o update e posteriormente o delete, de um ponto de interesse á escolha.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lista PIs</title>
</head>
<body>
<h2>Lista de Pontos de Interesse<h2>
<table border="1">
<thead>
<tr>
<td>Name</td>
<td>Details</td>
<td>Status</td>
</tr>
</thead>
<tbody>
<% pis.forEach(function(data) { %>
<tr>
<td><%= data.name %></td>
<td><%= data.details %></td>
<td><%= data.status %></td>
<td><a href="/api/edit/<%= data._id %>">Edit</a> |
<a href="/api/delete/<%= data._id %>">Delete</a></td>

</tr>
<% }) %>
</tbody>
<button><a href="/">Back</a></button>
</body>
</html>
listPIs.ejs

Notar as rotas referenciadas pelos links: ‘/api/edit/id’ e ‘/api/delete/id’ para os botões ‘Edit’ e ‘Delete’. O ‘id’ é passado no ciclo forEach através do objeto ‘pis’ que é propriedade do objeto anónimo na função ‘listAll’ em ‘\controller\apiController.js’.

. DELETE

Já temos a referencia á rota ‘/api/delete/:id no template listPIs.ejs’ precisamos apenas editar a função ‘delete’ de forma a que esta nos redireccione para a rota ‘/api/listall’ quando a operação for bem sucedida.

Em ‘\controllers\apiControllers.js’ editar a função ‘delete’ da seguinte forma:

// apaga 'pi' da BD, depois, redirecciona para '/api/listall'
exports.delete = function (req, res, next) {
// 'req.params.id'->devolve-me o parametro id na req
PI.findOneAndDelete({_id: req.params.id}).then(function(pi){
console.log("Registo eliminado com sucesso!");
res.redirect('/api/listall');
}).catch(next);
};
apiController.js

TESTAR AS OPERAÇÕES

. Criar um ponto de interesse

Browser-criar PI

. Listar pontos de interesse (ao clicar ‘submit’)

Browser — listall

. Editar (Restaurante Gaucho)

Browser-editar

. Atualizar (Restaurante Gaucho ao clicar ‘Edit’)

Browser-atualizar

. Apagar (Restaurante Gaucho ao clicar ‘Delete’)

Browser — Delete

. Listar Restaurantes próximos de lng=-80.87 & lat=25.80

Browser — Details

Optimizar código

Antes de passarmos á próxima fase do tutorial, fizemos algumas alterações no código, nos seguintes ficheiros:

  • \views\createPI.ejs :
<html lang=”en”>
<head>
<meta charset=”UTF-8">
<title>TESTE</title>
<link rel=”stylesheet” type=”text/css”
href=”/assets/colocoAquiMeu.css”>
</head>
<body>
<h2>Criar Ponto Interesse<h2>
<form action=”/api/interest” method=”POST”>
<input type=”text” placeholder=”name" name=”name">
<input type=”text” placeholder=”details" name=”details">
<input type=”text” placeholder=”longitude"
name=”geometry.coordinates[0]">
<input type="text" placeholder="latitude"
name="geometry.coordinates[1]">

<button type=”submit”>Submit</button>
</form>
</body>
</html>

Estas alterações permitem simplificar a função ‘create’ no controlador.

  • /controllers/apiController.js :
exports.create = function (req, res, next) {
PI.create(req.body).then(function(pi){
res.redirect('/api/listall');
}).catch(next);
};
  • \views\editPI.js :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Edit PI</title>
</head>
<body>
<h2>Edit</h2>
<form action="/api/update/<%= pi._id %>" method="POST">
<input type="text" value="<%= pi.name %>" name="name">
<input type="text" value="<%= pi.details %>" name="details">
<input type="text" value="<%= pi.status %>" name="status">
<input type="text" value="<%= pi.geometry.coordinates[0] %>"
name="geometry.coordinates[0]">
<input type="text" value="<%= pi.geometry.coordinates[1] %>"
name="geometry.coordinates[1]">
<button type="submit">Update</button>

</form>
</body>
</html>

Estas alterações permitem-nos editar também a latitude e longitude.

Browser — Edit
  • \views\listPIs.js :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lista PIs</title>
</head>
<body>
<h2>Lista de Pontos de Interesse<h2>
<table border="1">
<thead>
<tr>
<td>Name</td>
<td>Details</td>
<td>Status</td>
</tr>
</thead>
<tbody>
<% pis.forEach(function(data) { %>
<tr>
<td><%= data.name %></td>
<td><%= data.details %></td>
<td><%= data.status %></td>
<td><%= data.geometry.coordinates[0] %></td>
<td><%= data.geometry.coordinates[1] %></td>
<td><a href="/api/edit/<%= data._id %>">Edit</a> |
<a onclick="confirmation('Delete
record?!','/api/delete/<%= data._id %>')" href=#
>Delete</a></td>
</tr>
<% }) %>
</tbody>
<button><a href="/">Back</a></button>
<script>
function confirmation(msg, url) {
if(confirm(msg)) {
window.location.href = url;
}else{ false; }
}
</script>
</body>
</html>

Estas alterações permitem-nos listar a latitude e longitude e invocar uma box-confirm ao clicar em ‘delete’.

Browser — Delete

Criar e listar Pontos de Interesse baseado na localização do dispositivo usando HTML Geolocation API

No momento conseguimos criar um ponto de interesse introduzindo manualmente as coordenadas latitude e longitude, vamos de seguida implementar a ‘HTML Geolocation API’ que nos irá permitir capturar a posição geográfica do utilizador e guardá-la na base de dados, na submissão do formulário.

A propósito da API assinalar que esta é mais precisa em dispositivos com GPS e que atualmente só funciona em sites seguros como https.

Uso da HTML Geolocation API

Usaremos o método getCurrentPosition() para obter a posição do utilizador. Criaremos um evento ‘onclick’ que associaremos ao botão ‘submitesse evento chamará uma função que capturará a longitude e latitude do dispositivo usado pelo utilizador.

Em ‘/views/createPI.ejs’ adicionar o seguinte código:

No formulário fazer:

<%# "/api/create"-> rota de criação de novo registo %>
<form id="frm" action="/api/create" method="POST">
<input type="text" placeholder="name" value="MeuPI" name="name">
<input type="text" placeholder="details" value="Detalhes"
name="details">
<input type="text" id="input_lng" placeholder="longitude"
name="geometry.coordinates[0]">
<input type="text" id="input_lat" placeholder="latitude"
name="geometry.coordinates[1]">
<input type="button" onclick="onSubmitClk()" value="Submit">
</form>
<%# elemento onde vai ser renderizado a msg erro %>
<div id="map"></div>

Adicionámos valores hardcoded nos ‘inputs’ de name e details apenas para agilizar os testes e ids para o <form>, longitude e latitude para referenciarmos numa das funções (no script).

Adicionámos também um ‘<div id=”map”></div>’ que será o elemento onde vai ser renderizado a msg erro.

create.ejs

Adicionar uma tag ‘<script>’ e passar o seguinte código:

<script>function setLngLat(lng, lat){
// passa os valores de longitude e latitude nos inputs com
// ids:'input_lng' e 'input_lat'
document.getElementById('input_lng').setAttribute('value', lng);
document.getElementById('input_lat').setAttribute('value', lat);
// faz a submissão do formulário de id="frm"
document.getElementById("frm").submit();
}
function getGeoPos(callback){
// parametro para 'getCurrentPosition()'
function local(posicion){
var lng = posicion.coords.longitude;
var lat = posicion.coords.latitude;
// verifica se é função de callback
if (typeof callback === 'function') {
callback(lng, lat);
}
}

// chamada á API-HTML_Geolocation que devolve a posição da maquina
navigator.geolocation.getCurrentPosition(local);
}
function onSubmitClk(){
getGeoPos(setLngLat);
}
</script>

A função ‘setLngLat’ passa os valores de valores de longitude e latitude na submissão do formulário.

A função ‘getGeoPos(callback)’ obtém a posição geográfica do dispositivo a partir da API. Recebe a função ‘setLngLat(lng, lat)’ como calback uma vez que as operações de recolha das coordenadas (longitude e latitude) e de submissão desses valores no formulário têm de ser assíncronas, a primeira tem de ser completada antes de ocorrer a segunda, caso contrário os valores inseridos serão vazios (nulos).

A função ‘onSubmitClk()’ é a função chamada no evento ‘onclick’ do botão ‘submit’.

create.ejs

Obter latitude e longitude para query-string da rota:”/api/details/?lng=valorLng&lat=valorLat”:

Queremos criar um botão de nome ‘Near ’ que nos encaminhe para o end point ‘/api/details/’ e passar uma query-string com valores da latitude e longitude do dispositivo. Recordamos que é comparando com esses valores que os 3 pontos de interesse mais próximos (hardcoded) num raio de 100km (hardcoded) são devolvidos ao utilizador (rever função ‘details’ em ‘\controllers\apiControllers.js’).

Criar botão:

<button><a onclick="onNearClk('/api/details/')" href=# >Near</a></button>
create.ejs

Criar função ‘onNearClk’:

// Obter latitude e longitude para query-string da rota:"/api/details/?lng=lng&lat=lat"
function onNearClk(url){
// 'getElementById('map')'->referencia elemento onde vai
// renderizar a mensagem
var output = document.getElementById('map');
// Verificar se navegador suporta geolocalização
if (!navigator.geolocation) {
output.innerHTML = "<p>Your current browser does not support
Geolocation</p>";
}else{
// parametro para 'getCurrentPosition()'
function local(posicion){
const lat = posicion.coords.latitude;
const lng = posicion.coords.longitude;
location.href = url+'?lng='+lng+'&'+'lat='+lat;
}

// parametro para 'getCurrentPosition()'
function error(){
output.innerHTML = "<p>Unhable to find your location</p>";
}
// chamada á API-HTML_Geolocation que devolve a posição do
// dispositivo
navigator.geolocation.getCurrentPosition(local, error);
}
}
</script>

De notar que a função de callback local’ passará os valores de latitude e longitude para o atributo ‘href’ da anchor ‘<a>’ na execução do evento ‘onClick ’onde a função ‘onNearClk(url)’ é chamada.

create.ejs

Criámos este mesmo botão com as mesmas funcionalidade na view listPI.ejs’

listPI.ejs
listPI.ejs

Testar o código:

  • Criar PI
  • Listar PIs próximos

. Criar PI:

browser — criar PI

De notar que os campos Nome e Detalhes aparecem preenchidos. Ao clicar ‘submit ’ (sem preencher os campos restantes) encontraremos na lista de Pontos de Interesse, um registo com a longitude e latitude da nossa localização atual, no nosso caso temos:

browser — lista PIs

Ao clicar no botão ‘Near’ obteremos a lista dos 3 pontos de interesse mais próximos num raio de 100 km. De assinalar que de momento só existem 2 registos um correspondente á zona de Lisboa e outro á zona da Guarda (separados por mais de 100 km!).

. Lista de PIs próximos

browser — list near

Reparem na query string no navegador, os valores representam a nossa localização atual.

Renderizar o mapa das localizações com API Google Maps

Conseguimos captar a nossa posição geográfica atual, pretendemos a seguir renderizar um mapa a partir das coordenadas recebidas, para isso usaremos o ‘Google Maps Static API’.

Para usar o ‘Google Maps Static API’ são necessários realizar os seguintes passos:

. Criar um projeto em Console Developers da Google.

Console Developers da Google

. Seguir: ‘biblioteca’> ‘Maps JavaScript API’ > ‘Maps Static API’ >‘Ativar’

Console Developers da Google

. Seguir: ‘Credenciais’ > ‘criar credenciais’ > ‘chave de API’

Console Developers da Google
Console Developers da Google

. Copiar chave da API e guardá-la

. Voltar a ‘Maps Static APIe clicar ‘Gerenciar’

Console Developers da Google

. Seguir: ‘chave secreta de assinatura do URL’ > ‘Permitir uso sem assinatura’.

Console Developers da Google

. Voltar a ‘Maps JavaScript API’ e ativar.

Console Developers da Google

Existem várias outras opções que podem ser configuradas e convém que sejam exploradas, mas neste projeto não vamos utilizar.

Para ter acesso completo aos recursos da API, a Google pede para ativar ‘billing’ no projeto criado, aceder á pagina aqui!

. Seguir: ‘Selecionar ’(projeto)> ‘Criar conta de faturamento’ > ‘Aceitar condições’ > (preencher os campos)

billing
billing

Saber mais aqui!

De momento, no nosso projeto, não temos ativado o ‘billing’ isso impede-nos de renderizar mapas que refiram outras posições geográficas para além da nossa atual e impede ainda a utilização do parâmetro zoom na chamada da API (outros comportamentos imprevistos podem também ocorrer).

Implementação da Google Maps Static API

Passemos ao código.
Em ‘views/details.ejs’ editar HTML assim:

<tbody><% pis.forEach(function(data) { %>
<tr>
<td><%= data.name %></td>
<td><%= data.details %></td>
<td><%= data.status %></td>
<td><%= data.geometry.coordinates[0] %></td>
<td><%= data.geometry.coordinates[1] %></td>
<td><a href="/api/edit/<%= data._id %>">Edit</a>
<td><a onclick="onShowMapClck(
'<%= data.geometry.coordinates[0]%>',
'<%= data.geometry.coordinates[1]%>','<%= data.name %>')"
href=#>Show Map</a>
</tr>
<% }) %>
</tbody>
<button><a href="/">Back</a></button>
<%# tag que vai receber o nome do PI ao clicar 'Show map' %>
<p id="name"></p>
<%# elemento onde vai ser renderizado o mapa %>
<div id="map"></div>
<%# referenciar API-GOOGLE_STATIC_MAP %>
<script src="https://maps.googleapis.com/maps/api/js?key=MINHA_API_KEY">
</script>

Notar que criámos um evento que chamará uma função ‘onShowMapClck(lng, lat, name)’ que receberá como argumentos a ‘longitude’, latitude e o ‘nome ’dos PIs próximos num raio de 100 km (hardcoded).

Criámos ainda as tags ‘<p id=”name”></p> e <div id=”map”></div>’ para receberem o nome do PI e o mapa a renderizar…

Na tag ‘<script> referenciamos a ‘Google Maps Static API’, no valor ‘key passamos a chave que foi gerada para o nosso projeto.

details.ejs

No mesmo ficheiro vamos definir a função ‘onShowMapClck(lng, lat, name)’ que é chamada no evento do botão ‘Show Map’, acrescentar:

<script>function onShowMapClck(lng, lat, name){
// 'getElementById('map')'->referencia elemento onde vai
// renderizar o mapa
var outMap = document.getElementById('map');
var outName = document.getElementById('name');
// guarda imagem a partir da chamada á API-GOOGLE_STATIC_MAP
var imgURL = "https://maps.googleapis.com/maps/api/staticmap?center="+lat+","+lng+"&size=600x300&markers=color:red%7C"+lat+","+lng+"&key=MINHA_API_KEY";
// renderizar o mapa
outMap.innerHTML ="<img src='"+imgURL+"'>";
// passar o nome de PI
outName.innerHTML = name;
}
</script>

A função recebe como argumentos ‘longitude’ e ‘latitude ’que são passados como valores para a query-string de uma URL que referencia a ‘Google Maps Static API’. Não esquecer de passar no valor ‘key’, a chave gerada para o nosso projeto. O objeto ‘imgURL’ vai ser passado para a ‘<div id=”map”></div>’ que é onde o mapa será renderizado.

O argumento ‘name’ serve para passar na tag ‘<p id=”name”></p>’ o nome do ponto de interesse seleccionado.

details.ejs

Deixo de seguida um link de documentação | link.

Testar o código:

. Ir para a página ‘http://localhost:5000/’

browser — localhost:5000/

. Criar novo Ponto de Interesse clicando botão ‘Submit’.

browser — list

. Clicar ‘Near’ e na página de ‘detalhes’ clicar ‘show map’.

browser — render map

. Eis o resultado que mostrou quando estávamos no ‘Café Ponto de Encontro’ a mais de 100 km de Lisboa.

browser — render map

Nota: O resultado exibido é referente ao estado da B.D. na altura, desde então já foram criados e apagados vários registos.

Criação de um sistema de autenticação login e registo com Passport

Vamos em seguida criar um sistema de login e registo. Usaremos um módulo chamado Passport que é um middleware de autenticação criado para Node.js

Para esta parte do projeto necessitaremos acrescentar as seguintes dependencias:

  • “bcryptjs”— usado para encriptação de passwords (hashing)
  • “connect-flash”— usado para uso de mensagens flash
  • “express-ejs-layouts”—para complementar ejs que não possui layouts
  • “express-session”— usado para uso de mensagens flash
  • “passport” — middleware de autenticação
  • “passport-local”— a estratégia de autenticação passport que iremos usar(usa base de dados local).

No terminal aberto na localização do projeto fazer:

npm install --save bcryptjs connect-flash express-ejs-layouts express-session passport passport-local

Consultar ‘package.json’:

package.json

Vamos incluir os módulos instalados em ‘app.js’:

// incluir as dependências instaladas
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const expressLayouts = require('express-ejs-layouts');
const passport = require('passport');
const flash = require('connect-flash');
const session = require('express-session');
const app = express();
...

Necessitamos criar middleware para:

  • Express session
  • ExpressLayouts
  • Connect flash
  • Global variables
  • Route-handlers
  • Passport

. ExpressLayouts middleware

. Em ‘app.js’ adicionar o seguinte código:

// define '/views/layout' como main-layout! (é renderizada na raiz)
app.use(expressLayouts);

. Route-handlers middleware

. Em ‘app.js’ adicionar o seguinte código:

...
// ROUTE-HANDLERS middleware
const api = require('./routes/api');
const users = require('./routes/users');
const index = require('./routes/index');
app.use('/api', api);
app.use('/users', users);
app.use('/', index);
...

De notar que os ficheiros ‘users.js’ e ‘index.js’ estão por criar! Serão criados posteriormente.

ExpressLayouts + Route-handlers middleware

. Express session middleware

. Em ‘app.js’ adicionar o seguinte código:

...
// Express session middleware
app.use(
session({
secret: 'secret',
resave: true,
saveUninitialized: true
})
);
...

. Passport middleware

. Em ‘app.js’ adicionar o seguinte código:

// Passport middleware
app.use(passport.initialize());
app.use(passport.session());

. Connect flash middleware

. Em ‘app.js’ adicionar o seguinte código:

// Connect flash middleware
app.use(flash());

. Global variables middleware

. Em ‘app.js’ adicionar o seguinte código:

// Global variables middleware
app.use(function(req, res, next) {
// ‘res.locals’->é a forma de criar variáveis ou funções globais
res.locals.success_msg = req.flash(‘success_msg’);
res.locals.error_msg = req.flash(‘error_msg’);

// passport tem as suas próprias flash-msgs
// que passa em ‘flash(‘error’)’, assim faço overwrite
res.locals.error = req.flash(‘error’);
next();
});

A ordem do middleware é importante, como já anteriormente referimos! Adicionar o middleware assim:

middleware

. Acrescentar os ficheiros necessários a esta parte do projeto

Criar os seguintes ficheiros:

  • /config/auth.js —receberá código para autenticação das rotas
  • /config/passport.js —receberá código para Passport-local-strategy
  • /controller/indexController.js — controlador para a rota ‘index.js’.
  • /controller/usersContoller.js — controlador para a rota ‘users.js’.
  • /public/assets — para receber ficheiros externos como ‘styles.css’.
  • /routes/index.js — rota para página ‘Home’.
  • /routes/users.js — rotas para para páginas ‘Login ’e ‘Register’.
  • /views/dashboard.ejs — página dashboard-teste.
  • /views/layout.ejs layout que recebe o ‘<body>’ das restantes páginas.
  • /views/login.ejs — página de Login.
  • /views/register.ejs — página de Register.
  • /views/welcome.ejs — página Home.
estrutura de pastas e ficheiros

. Estruturar o nosso layout

Antes de passar ao código, vamos ao site de bootswatch escolher um tema (journal) e copiar o endereço do CSS a partir do botão ‘Download’(fazer right-click+copiar endereço) para passar como referencia na tag ‘<link>

site-bootswatch

Vamos a fontawesome copiar a tag <link>’.

site-fontawsome

Para usar flash-messages necessitamos dos scripts em bootstrap dismissable alerts navegar para a pagina e guardar os scripts.

Em /views/layout.ejs acrescentar o seguinte código:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />

<%# FONTAWSOME %>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<%# BOOTSWATCH %>
<link rel="stylesheet" href="https://bootswatch.com/4/journal/bootstrap.min.css">
<title>Pontos de Interesse App</title>
</head>
<body>

<%# output das views que queremos mostrar %>
<div class="container"><%- body %></div>
<%# bootstrap dismissable alerts %>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut"
crossorigin="anonymous">
</script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k"
crossorigin="anonymous">
</script>
</body>
</html>

. Navegar até pagina Home

Ir para ‘/views/welcome.ejs’ e acrescentar o seguinte código:

<div class="row mt-5">
<div class="col-md-6 m-auto">
<div class="card card-body text-center">
<h1><i class="fab fa-node-js fa-3x"></i></h1>
<p>Create an account or login</p>
<a href="/users/register" class="btn btn-primary btn-block mb-2">Register</a>
<a href="/users/login" class="btn btn-secondary btn-block">Login</a>
</div>
</div>
</div>

A tag ‘<i class="fab fa-node-js fa-3x"></i>’ coloca o icone de nodeJS na página.

Em ‘/routes/index.js’ acrescentar o seguinte código:

const express = require (‘express’);const router = express.Router();// importa controlador
const indexController = require(‘../controllers/indexController’);
// index-route (Home-page)
router.get(‘/’, indexController.index);
// create-route
router.get(‘/create’, indexController.create);
module.exports = router;

Em ‘controllers/indexController.js’:

// exporta f.teste, chamada em /routes/index
exports.index = function (req, res) {
res.render('welcome');
};
// página 'create'
exports.create = function (req, res) {
res.render('createPI');

};

Quando no browser introduzimos o endereço: http://localhost:5000/:

home page

. Navegar até pagina Register:

Ir para ‘/views/register.ejs’ e acrescentar o seguinte código:

<div class="row mt-5"><div class="col-md-6 m-auto"><div class="card card-body">
<h1 class="text-center mb-3">
<i class="fas fa-user-plus"></i> Register
</h1>
<form action="/users/register" method="POST">
<div class="form-group">
<label for="name">Name</label>
<input
type="name"
id="name"
name="name"
class="form-control"
placeholder="Enter Name"
value="<%= typeof name != 'undefined' ? name : '' %>"
/>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
class="form-control"
placeholder="Enter Email"
value="<%= typeof email != 'undefined' ? email : '' %>"
/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="Create Password"
value="<%= typeof password != 'undefined' ? password : '' %>"
/>
</div>
<div class="form-group">
<label for="password2">Confirm Password</label>
<input
type="password"
id="password2"
name="password2"
class="form-control"
placeholder="Confirm Password"
value="<%= typeof password2 != 'undefined' ? password2 : '' %>"
/>
</div>
<button type="submit" class="btn btn-primary btn-block">
Register </button>
</form>
<p class="lead mt-4">Have An Account?
<a href="/users/login">Login</a></p>
</div>
</div>
</div>

Assinalar que no atributo dos inputs — Exemplo: ‘value=”<%= typeof name != ‘undefined’ ? name : ‘’ %>”’ diz que se existir um ‘nome ’no ‘form’ manter o ‘nome ’mesmo que a autenticação falhe.

Em ‘/routes/users.js’ acrescentar o seguinte código:

const express = require ('express');
const router = express.Router();
// importa controlador
const usersController = require('../controllers/usersController');
// Register Page
router.get('/register', usersController.GETregister);
module.exports = router;

Em ‘controllers/usersController.js’ acrescentar o seguinte código:

// register
exports.GETregister = function (req, res) {
res.render('register');
};

Ao clicar no botão ‘Register’ renderiza a página assim:

página register

. Navegar até á página Login:

Ir para ‘/views/login.ejs’ e acrescentar o seguinte código:

<div class="row mt-5"> 
<div class="col-md-6 m-auto">
<div class="card card-body">
<h1 class="text-center mb-3">
<i class="fas fa-sign-in-alt">/i> Login</h1>
<form action="/users/login" method="POST">
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
class="form-control"
placeholder="Enter Email"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="Enter Password"/>
</div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
</form>
<p class="lead mt-4">
No Account? <a href="/users/register">Register</a>
</p>
</div>
</div>
</div>

Em ‘/routes/users.js’ acrescentar o seguinte código:

const express = require ('express');
const router = express.Router();
// importa controlador
const usersController = require('../controllers/usersController');
// Login Page
router.get('/login', usersController.GETlogin);
// Register Page
router.get('/register', usersController.GETregister);
module.exports = router;

Em ‘controllers/usersController.js’ acrescentar o seguinte código:

// login
exports.GETlogin = function (req, res) {
res.render('login');
};
// register
exports.GETregister = function (req, res) {
res.render('register');
};

Ao clicar no botão ‘Login’ renderiza a página assim:

pagina login

. Validação dos campos no formulário Registo

Em ‘/routes/users.js’ editar o código como:

const express = require ('express');
const router = express.Router();
// importa controlador: 'usersController'
const usersController = require('../controllers/usersController');
// render da Login-Page
router.get('/login', usersController.login);
// render da Register-Page
router.get('/register', usersController.GETregister);
// registo na BD
router.post('/register', usersController.POSTregister);
module.exports = router;

Em ‘controllers/usersController.js’ acrescentar o seguinte código:

// login
exports.login = function (req, res) {
res.render('login');
};
// GETregister
exports.GETregister = function (req, res) {
res.render('register');
};
// POSTregister-form(validação)
exports.POSTregister = function (req, res) {
// os valores de req.body são separados individualmente em
// variáveis
const { name, email, password, password2 } = req.body;
let errors = [];
// array de erros
// verificar se todos os campos estão preenchidos
if (!name || !email || !password || !password2) {
errors.push({ msg: 'Please enter all fields' });
}


// verificar se as passes são iguais
if (password != password2) {
errors.push({ msg: 'Passwords do not match' });
}
// verificar tamanho da pass
if (password.length < 3) {
errors.push({ msg: 'Password must be at least 6 characters' });
}
// Se há erros..
if (errors.length > 0) {
// render da página com os valores do 'form' + erros
res.render('register', {
errors,
name,
email,
password,
password2
});
} else {
res.send('REGISTER_SUCESSO!');
}
};

Ao preencher os campos do registo, com todos os campos preenchidos, passwords iguais e passwords com mínimo de 3 carateres, então não tem erros e envia mensagem ‘REGISTER_SUCESSO!’.

Caso haja erros, no momento, a página não os assiná-la.

register
register-success

. Passar mensagens de erro para o utilizador

Para passar mensagens de erro de cada vez que a validação falha temos de criar uma pasta que nomearemos de ‘partials’ em ‘\views’, que receberá um ficheiro ‘messages.ejs’ onde são tratados os erros. Esse ficheiro será incluído no template ‘register.ejs’ e ‘login.ejs’ com

 <% include ./partials/messages %>

Vamos começar por incluir o template ‘messages.ejs’ em ‘register.ejs’ e ‘login.ejs’.

register.ejs
login.ejs

Em seguida adicionar o seguinte código em ‘\views\messages.ejs’:

<%# 'typeof' devolve tipo da variavel, 'undefined'->errors_vazio! %><% if(typeof errors != 'undefined'){ %>
<% errors.forEach(function(error) { %>
<%= error.msg %>
<% }); %>
<% } %>

Agora ao clicar no botão ‘Register’ da aplicação, se o formulário tiver erros, esses erros são exibidos no topo do formulário.

register

Notar que o campo nome está vazio e as passwords não coincidem!

Alerts Bootstrap

Vamos melhorar o aspeto das msgs com o ‘bootstrap-alert’ markup.

Ir até getbootstrap.com > get started > components > alerts > dismissing (link) copiar a ‘<div>’ de exemplo exibida e passar para o nosso código assim:

<%# 'typeof' devolve tipo variavel, 'undefined'->errors_vazio! %>
<% if(typeof errors != 'undefined'){ %>
<% errors.forEach(function(error) { %>
<div class="alert alert-warning alert-dismissible fade show"
role="alert">

<%= error.msg %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">&times;</span></button>
</div>
<% }); %>
<% } %>

Ao clicar no botão ‘Register’ da aplicação, se o formulário tiver erros, esses erros são exibidos assim:

register

. Criar modelo user

Vamos criar o modelo ‘user’ para receber os dados do formulário e guardar na Base de Dados.

Na pasta ‘/models’ criar um ficheiro designado por ‘user.js’ e adicionar o seguinte código:

const mongoose = require('mongoose');const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', UserSchema);module.exports = User;

Em seguida vamos importar o modelo em ‘controller\userController.js’ para podermos fazer operações na base de dados. Adicionar:

// importar modelo User
const User = require('../models/User');

. Criar novo ‘user’ com password encriptada

Para proceder á encriptação da password temos de primeiro importar ‘bcryptjs’.

Em ‘controller\userController.js’ adicionar:

const bcrypt = require('bcryptjs');

No mesmo ficheiro editar a função ‘POSTregister’ desta forma:

// POSTregister-form(validação)
exports.POSTregister = function (req, res) {
// os valores de req.body são separados individualmente em
// variáveis
const { name, email, password, password2 } = req.body;
let errors = []; // array de erros
// verificar se todos os campos estão preenchidos
if (!name || !email || !password || !password2) {
errors.push({ msg: 'Please enter all fields' });
}

// verificar se as passes são iguais
if (password != password2) {
errors.push({ msg: 'Passwords do not match' });
}
// verificar tamanho da pass
if (password.length < 3) {
errors.push({ msg: 'Password must be at least 6 characters' });
}
// Se há erros..
if (errors.length > 0) {
// render da página com os valores do 'form' + erros
res.render('register', {
errors,
name,
email,
password,
password2
});
} else {

// procura User_Duplicado, dps devolve o 'user'
User.findOne({ email: email }).then(user => {
// se 'user' já existe renderiza 'register.ejs'com erro
if (user) {
errors.push({ msg: 'Email already exists' });
res.render('register', {
errors,
name,
email,
password,
password2
});

// senão há user_duplicado cria novo_user
} else {
const newUser = new User({
name,
email,
password
});

// encripta passw e guarda novo_user
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then(user => {
res.redirect('/users/login');
})
.catch(err => console.log(err));
});
});
}
});
}
};

Vamos criar um user a partir do formulário:

criar user

Na base de dados (ATLAS MongoDB) deverá mostrar algo como:

atlas

Notar que a password está encriptada!

Se tentar-mos criar outro user com o mesmo e-mail teremos:

e-mail duplicado

No momento, somos redireccionados para a página de login quando o registo é bem sucedido, queremos agora, para além disso, passar uma mensagem de sucesso. Para conseguir isso faremos uso dos módulos ‘connect-flash’ e ‘express-session’.

Em ‘app.js ’ o import dos módulos e o middleware encontra-se já implementado, falta chamar a flash-message na função ‘POSTregister definida em ‘usersController.js’ e passá-la em ‘\views\partials\messages.js’.

Em ‘\controllers\usersController.js\’ editar a função ‘POSTregister’ assim:

// POSTregister-form(validação)
exports.POSTregister = function (req, res) {
// os valores de req.body são separados individualmente em
// variáveis
const { name, email, password, password2 } = req.body;
let errors = []; // array de erros
// verificar se todos os campos estão preenchidos
if (!name || !email || !password || !password2) {
errors.push({ msg: 'Please enter all fields' });
}

// verificar se as passes são iguais
if (password != password2) {
errors.push({ msg: 'Passwords do not match' });
}
// verificar tamanho da pass
if (password.length < 3) {
errors.push({ msg: 'Password must be at least 6 characters' });
}
// Se há erros..
if (errors.length > 0) {
// render da página com os valores do 'form' + erros
res.render('register', {
errors,
name,
email,
password,
password2
});
} else {
// procura User_Duplicado, dps devolve o 'user'
User.findOne({ email: email }).then(user => {
// se 'user' já existe renderiza 'register.ejs'com erro
if (user) {
errors.push({ msg: 'Email already exists' });
res.render('register', {
errors,
name,
email,
password,
password2
});
// senão há user_duplicado cria novo_user
} else {
const newUser = new User({
name,
email,
password
});
// encripta passw e guarda novo_user
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then(user => {
// flash-message-register-sucess
req.flash(
'success_msg','You are now registered!'
);
res.redirect('/users/login');
})
.catch(err => console.log(err));
});
});
}
});
}
};

Em ‘\views\partials\messages.js’ acrescentar:

<%# 'typeof' devolve tipo variavel, 'undefined'->errors_vazio! %>
<% if(typeof errors != 'undefined'){ %>
<% errors.forEach(function(error) { %>
<div class="alert alert-warning alert-dismissible fade show"
role="alert">
<%= error.msg %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">&times;</span></button>
</div>
<% }); %>
<% } %>
<%# var-globais(nulas se não forem chamadas) %>
<% if(success_msg != ''){ %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<%= success_msg %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">&times;</span> </button>
</div>
<% } %>
<% if(error_msg != ''){ %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<%= error_msg %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">&times;</span> </button>
</div>
<% } %>
<% if(error != ''){ %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<%= error %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">&times;</span> </button>
</div>
<% } %>

Notar as variáveis ‘success_msg’ e ‘error_msg’ que são variáveis globais definidas em ‘app.js’ no middleware.

Se agora fizer-mos o registo de um utilizador, ao clicar no botão ‘register’, teremos algo como:

success-register

. Passport e Login

Usaremos passport na versão local-strategy para tratar da autenticação. Apenas utilizadores autenticados têm acesso ás rotas. Saber mais!

Numa aplicação da Web, as credenciais usadas para autenticar um utilizador só serão transmitidas durante a solicitação de login. Se a autenticação for bem-sucedida, uma sessão será estabelecida e mantida por meio de um cookie configurado no browser do utilizador.

Cada solicitação subsequente não terá credenciais, mas sim o cookie exclusivo que identifica a sessão. Para suportar sessões de login, o Passport serializará e desserializará as instâncias do utilizador para e da sessão.

No ficheiro ‘passport.js’ vamos construir a estratégia local do Passport e implementar a serialização e desserialização do utilizador.

Passemos ao código!

Em ‘config/passport.js’ adicionar :

const LocalStrategy = require('passport-local').Strategy;
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// importar User-model
const User = require('../models/User');
// o import desta função será em usersController.js para POSTlogin e // logout, 'passport' vai ser passado a partir de app.js
module.exports = function(passport) {
passport.use(new LocalStrategy({ usernameField: 'email' }, (email,
password, done) => {

// e-mail coincide?
User.findOne({
email: email
}).then(user => {
if (!user) {

// 'done' callback(recebido em cima)
return done(null, false, { message: 'Unregistered e-mail' });
}

// password e-mail coincidem?
// bcrypt desencripta password(recebida em cima) e compara
// password_introduzida
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Password incorrect'});
}
});
});
})
);

// metodo para serialização do 'user'
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// metodo para desserialização do 'user'
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
};

De notar que em ‘module.exports = function(passport) {…}’ o argumento ‘passport’ não está configurado, tal configuração será passada em ‘app.js’.

Em ‘app.js’ acrescentar:

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const expressLayouts = require('express-ejs-layouts');
const flash = require('connect-flash');
const session = require('express-session');
const passport = require('passport');
const app = express();// Passo 'require('passport')' para './config/passport'
require('./config/passport')(passport);

Vamos implementar uma rota para a submissão do formulário Login.

Em ‘\routes\users.js’ acrescentar:

router.post('/login', usersController.POSTlogin);
routes\users.js

Quando fazemos o submit do formulário Login queremos que a rota ‘users/login’ use olocal strategy definido em ‘\config\passport.js

Em ‘controller\userController.js’ acrescentar:

// POSTlogin
exports.POSTlogin = function (req, res) {
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
})(req, res, next);
};
userController.js

Se tentarmos agora fazer login com um utilizador não registado, a página é novamente renderizada e apresentar-se-á assim:

login

Quando o login for bem sucedido a página dashboard (apenas de teste) será renderizada. Vamos adicionar o seguinte código a ‘views\dashboard’.

<h1 class="mt-4">Dashboard</h1>
<p class="lead mb-3">Welcome </p>
<a href="/users/logout" class="btn btn-secondary">Logout</a>

De notar que a rota ‘/users/logout’ não foi ainda criada!

Se fizermos um login válido teremos:

dashboard

Passemos ao logout.

Necessitamos de criar uma rota. Em ‘\routes\users’ acrescentar:

// logout
router.get('/logout', usersController.logout);
routes\users

Em ‘controllers\usersController.js’ acrescentar:

// logout
exports.logout = function (req, res) {
req.logout();
req.flash('success_msg', 'You are logged out');
res.redirect('/users/login');
};
usersController.js

Ao clicar no botão ‘logout’ somos redireccionados para a página de login, com uma mensagem de sucesso.

logout-success

Proteger rotas

No momento, se por exemplo introduzirmos o endereço: http://localhost:5000/dashboard no navegador, seremos encaminhados para dashboard, sem necessitarmos de fazer autenticação.

Para proteger as rotas usaremos ensureAuthenticated de Passport.

Em ‘\config\Auth.js’ acrescentar o seguinte código:

// objeto anónimo com propriedade 'ensureAuthenticated'
module.exports = {
ensureAuthenticated: function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
// msg erro
req.flash('error_msg', 'Please log in to view that resource');
res.redirect('/users/login');
}
};

Incluiremos esta propriedade como parâmetro nas rotas que quisermos proteger. Exemplo: se quisermos que apenas utilizadores autenticados acedam á página Dashboard basta em ‘routes\index.js’ editar o código como:

const express = require ('express');
const router = express.Router();
// importa a propriedade do objeto anónimo de '../config/auth'
const { ensureAuthenticated } = require('../config/auth');

// importa controlador
const indexController = require('../controllers/indexController');
...
// dashboard
router.get('/dashboard', ensureAuthenticated,
indexController.dashboard);

...

Agora se no browser introduzirmos o endereço http://localhost:5000/dashboard obteremos:

acesso negado — sem autenticação

Incluir informação da distancia entre a localização atual do utilizador e os pontos de interesse próximos de si

Implementar esta funcionalidade é bastante simples.

Recordemos primeiro que objeto JSON a função ‘details’ em ‘/controllers/apiController’ retorna na rota: ‘http://localhost:5000/api/details/?lng=lngVal&lat=latVal’.

Vamos editar temporariamente a função ‘details’ com:

module.exports.details = (req, res, next) =>{
let lng = parseFloat(req.query.lng);
let lat = parseFloat(req.query.lat);
const maxDist = 300000; // 300km
PI.aggregate([{
'$geoNear': {
'near': { 'type': 'Point',
'coordinates': [parseFloat(lng), parseFloat(lat)] },
'spherical': true,
'distanceField': 'dist',
'limit': 5,
'maxDistance': maxDist
}
}])
.then(pi => res.send(pi))
// .then(pi => res.render('details', {pis: pi}))
.catch(next);
};

Recorrendo ao Postman:

  • Seleccionar a rota acima com valores de query-string correspondentes a uma localização próxima de pelo menos um Ponto de Interesse presente na base de dados.
  • Premir botão ‘Send

Exemplo de resultado:

postman

Notar que o objeto contém uma propriedade de nome “dist”. O valor numérico de “dist” representa a distancia do utilizador ao ponto de interesse, no exemplo acima o “Café Modem”, está á distancia de 18538.772… metros da localização correspondente á latitude e longitude introduzidas na query-string.

Para visualizarmos esta informação na página de details (Near By) editar a view ‘details.ejs’ como:

...
<% pis.forEach(function(data) { %>
<tr>
<td><%= data.name %></td>
<td><%= data.details %></td>
<td><%= Math.floor(data.dist)%> m</td>
...

Incluir data de criação de um pontosde interesse

Para incluir a data de criação de um ponto de interesse, temos de atualizar o modelo ‘PImodel’ com uma propriedade do tipo date.

Em ‘models/PImodel.js’ editar como:

...
const PISchema = new Schema({
date: {
type: Date,
default: Date.now
},

name: {
type: String,
required: [true, '*Campo obrigatório!']
},
details: {
type: String
},
status: {
type: Boolean,
default: true
},
geometry: GeoSchema
});

Notar que por omissão ‘date’ assumirá o valor da data atual.

Agora basta nas viewslisPIs.ejs’ e ‘details.ejs’ acrescentar um novo ‘<td>’ no ciclo ‘forEach’ assim:

<% pis.forEach(function(data) { %>
<tr>
...
<td><%= data.date.toString().substring(0, 24) %></td>
...
</tr>
<% }) %>

Notar a conversão dos dados recebidos para string e o “corte” da string a partir do carater 24 para excluir a informação ‘GMT+0100 (GMT Daylight Time)’ de por Exemplo: ‘Sat May 11 2019 22:53:34 GMT+0100 (GMT Daylight Time)’.

date time

Colocação da aplicação num serviço na Cloud

O serviço da cloud que iremos utilizar é o Heroku. Existe um pré-requisito necessário para esta operação que é ter o GitHub instalado. Saber mais aqui!

Para hospedar a aplicação no Heroku seguir os seguintes passos:

  • Criar uma conta no Heroku.
  • No site criar uma nova app:
criar app no heroku
  • Seguir para Heroku CLI
  • Baixar o instalador
  • Instalar

Depois de instalado o CLI, será possível aceder á conta que criámos via Git Bash.

Concluímos a instalação no terminal do Git Bash com npm install -g heroku.

Saber mais!

terminal git bash no VS code
  • Aceder á conta Heroku via terminal: heroku login
heroku login
logged in
  • Inicializar a aplicação como repositório Git: git init (garantir que estamos na pasta do projeto!)
  • Ligar a aplicação ao repositório do Heroku: heroku git:remote -a pontosinteresse (‘pontosinteresse’ foi o nome que demos á app no heroku)
  • Adicionar a nossa aplicação ao repositório
$ git add .
$ git commit -am "first commit"
$ git push heroku master
  • Criar um ficheiro com nome ‘Procfile na raiz do projeto e editar como:
// app.js-> nome da nossa aplicação principal
web: node app.js
Procfile
  • Garantir que o servidor está á escuta no porto providenciado pelo Heroku.

Em ‘app.js’ editar ficheiro como:

// 'process.env.port': o porto é providenciado por Heroku
let port = 5000;
app.listen(process.env.PORT || port, () =>{
console.log('Servidor em execução no porto: '+ port);
});
  • Submeter as mudanças efetuadas na aplicação no repositório
$ git add .
$ git commit -am "meu novo commit"
$ git push heroku master

Se tudo correu bem, a aplicação ficou devidamente hospedada e disponível online. No dashboard da Heroku encontramos o botão ‘open app’ e ao clicar, encontraremos a nossa aplicação.

open app — botão
app — online

Separar a aplicação da API

Queremos disponibilizar a API da aplicação a utilizadores autenticados, queremos que as rotas da api sejam referenciadas como …/api/… (Ex: ‘http://localhost:5000/api/listall’) e a aplicação como …/… (Ex: ‘http://localhost:5000/listall’) para tal devemos fazer o seguinte:

  • Copiar as funções de apiController para indexController (não esquecer de importar o modelo PI).
  • Em indexController todo e qualquer redirect pretendido deve ser feito a partir da raiz (‘/’) Ex: ‘res.redirect(‘/listall’)
  • Em ‘routes\index.js’ criar as rotas necessárias que referenciem as funções copiadas para indexController e proteger as rotas com a propriedade ensureAuthenticated importada de ‘\config\Auth.js’.
  • Nas views, todos os endereços começados por ‘/api/…’ devem passar a ser ‘/…
  • Em apiController toda e qualquer função, deve enviar na resposta, o objeto solicitado no end-point, tal qual faziamos com o Postman antes de criarmos o front-end da aplicação — Exemplo:
// listar todos os pontos de interesse da BD
exports.listAll = function (req, res, next) {
PI.find({}).then(function(pi){
// res.render('listPIs', {pis: pi}); -> no front-end
res.send(pi);
}).catch(next);
};
apiController — excerto
indexController — excerto
routes\index.js
listPIs.ejs — excerto

Separada a aplicação da api podemos oferecer dois exemplos, que nesta altura para quem acompanhou o tutorial desde o inicio, não são novidade. Se introduzirmos o endereço: http://localhost:5000/api/listall recebemos o seguinte resultado:

pedido de autenticação

Porque a rota está protegida é-nos pedido fazer a autenticação.

Se nos autenticar-mos e fizer-mos de novo o mesmo pedido, recebemos o seguinte resultado:

unico documento na base de dados

Se fizer-mos logout e introduzir-mos o endereço: http://localhost:5000/listall, recebemos o seguinte resultado:

pedido de autenticação

Se nos autenticar-mos e fizer-mos de novo o mesmo pedido, recebemos o seguinte resultado:

unico documento na base de dados

Melhoramentos no layout

Mencionaremos alguns melhoramentos efetuados no layout da aplicação.

Em ‘\public\assets\styles’ criámos as seguintes classes para criar os botões:

style.css — botões

Na view ‘dashboard.ejs’ (que antes se chamava ‘create.ejs’) incluímos um video com o seguinte código:

dashboard.ejs

Tornámos o mapa da google responsivo criando e usando as seguintes classes em ‘styles.css’:

styles.css — map

Em ‘\views\details.ejs’ usar a class ‘map-responsive’ como:

dashboard.ejs

As restantes modificações foram um exercício de exploração do tema ‘Flatlyde ‘bootswatch’ que usámos em ‘views\bootswatch.ejs’.

Screenshots da nossa aplicação final

welcome-page
register-page
login-page
dashboard-page
nearby-page
edit-page
nearby-page (show)

E chegamos ao fim deste Tutorial, esperamos que tenham gostado!

Até á próxima!

--

--