diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..5e0e574 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "public/bower" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a88195f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +bower +build + +*~ +*.log +*.env +*.migrate diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..740279e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Quentin PANISSIER + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..ae7366d --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node web.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..fc65695 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 ( https://github.com/hardware/ovh-availability/releases/tag/v1.0.0 ) diff --git a/bin/cron b/bin/cron new file mode 100755 index 0000000..0ad7f02 --- /dev/null +++ b/bin/cron @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +var argv = require('optimist').argv; +var request = require('request'); + +var options = { + method: 'GET', + uri: process.env.APP_URL + 'cron/' + argv.task + '/' + process.env.CRON_KEY +}; + +request(options, function( error, response, body ) { + + if( ! error && response.statusCode == 200 ) console.log( body ); + +}); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..cd094d6 --- /dev/null +++ b/bower.json @@ -0,0 +1,23 @@ +{ + "name": "ovh-availability", + "version": "1.0.0", + "description": "Application permettant de connaitre la disponibilité des serveurs d'OVH (SoYourStart, Kimsufi)", + "main": "web.js", + "moduleType": [ + "node" + ], + "license": "MIT", + "homepage": "https://github.com/hardware/ovh-availability", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components" + ], + "dependencies": { + "jquery": "1.11.1", + "bootstrap": "3.2.0", + "html5shiv": "3.7.2", + "respond": "1.4.2" + } +} diff --git a/database/schema.dbm b/database/schema.dbm new file mode 100644 index 0000000..f1b9d59 --- /dev/null +++ b/database/schema.dbm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
diff --git a/migrations/001-init.js b/migrations/001-init.js new file mode 100644 index 0000000..c98328c --- /dev/null +++ b/migrations/001-init.js @@ -0,0 +1,309 @@ +var pg = require('pg'); +var S = require('string'); +var async = require('async'); + +var DB_URL = process.env.DATABASE_URL || 'tcp://localhost:5432/ovh-availability'; + +exports.up = function( next ) { + + pg.connect(DB_URL, function( dbErr, client, done ) { + + async.series([ + + // Création de l'énumération "SERVERS_TYPE" + function( callback ) { + + var q = client.query("CREATE TYPE public.servers_type AS ENUM ('sys','kimsufi')"); + + q.on('end', function() { + callback(); + }); + + }, + + // Création de l'énumération "REQUEST_STATE" + function( callback ) { + + var q = client.query("CREATE TYPE public.request_state AS ENUM ('pending','done')"); + + q.on('end', function() { + callback(); + }); + + }, + + // Création de la table "SERVERS" + function( callback ) { + + var q = client.query(" \ + CREATE TABLE public.servers( \ + id serial NOT NULL, \ + type public.servers_type NOT NULL, \ + name character varying(100) NOT NULL, \ + reference character varying(10) NOT NULL, \ + CONSTRAINT id_servers_pm PRIMARY KEY (id), \ + CONSTRAINT unique_ref UNIQUE (reference) \ + )"); + + q.on('end', function() { + callback(); + }); + + }, + + // Création de la table "REQUESTS" + function( callback ) { + + var q = client.query(" \ + CREATE TABLE public.requests( \ + id serial NOT NULL, \ + reference character varying(10) NOT NULL, \ + mail character varying(100) NOT NULL, \ + date timestamp with time zone NOT NULL DEFAULT NOW(), \ + state public.request_state NOT NULL DEFAULT 'pending', \ + CONSTRAINT id_requests_pm PRIMARY KEY (id) \ + )"); + + q.on('end', function() { + callback(); + }); + + }, + + // Création de la relation entre la table 'servers' et 'requests' + function( callback ) { + + var q = client.query("ALTER TABLE public.requests ADD CONSTRAINT ref_servers_fk FOREIGN KEY (reference) \ + REFERENCES public.servers (reference) MATCH FULL \ + ON DELETE NO ACTION ON UPDATE NO ACTION;"); + + q.on('end', function() { + callback(); + }); + + }, + + // Ajout des serveurs d'OVH dans la table 'servers' + function( callback ) { + + async.parallel([ + + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SAT-1', '143sys4')", function( err, result ) { + callback(); + }); + }, + + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SAT-1', '143sys4')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SSD-1', '143sys13')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SAT-2', '143sys1')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SSD-2', '143sys10')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SAT-3', '143sys2')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SSD-3', '143sys11')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SAT-4', '143sys3')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'E3-SSD-4', '143sys12')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-1', '142sys4')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-2', '142sys5')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-4', '142sys8')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-5', '142sys6')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-5S', '142sys10')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-6', '142sys7')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'SYS-IP-6S', '142sys9')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'BK-8T', '141bk1')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'BK-24T', '141bk2')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'GAME-1', '141game1')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'GAME-2', '141game2')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('sys', 'GAME-3', '141game3')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-1', '150sk10')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-2a', '150sk20')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-2b', '150sk21')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-2c', '150sk22')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-3', '150sk30')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-4', '150sk40')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-5', '150sk50')", function( err, result ) { + callback(); + }); + }, + function( callback ) { + client.query("INSERT INTO servers (type, name, reference) VALUES ('kimsufi', 'KS-6', '150sk60')", function( err, result ) { + callback(); + }); + } + ], function( err, results ) { + + callback(); + + }); + + } + + ], function( err, results ) { + + if( ! err ) next(); + + }); + + }); + +}; + +exports.down = function( next ) { + + pg.connect(DB_URL, function( dbErr, client, done ) { + + async.series([ + + function( callback ) { + + var q = client.query("DROP TABLE servers CASCADE"); + + q.on('end', function() { + callback(); + }); + + }, + + function( callback ) { + + var q = client.query("DROP TABLE requests CASCADE"); + + q.on('end', function() { + callback(); + }); + + }, + + function( callback ) { + + var q = client.query("DROP TYPE servers_type"); + + q.on('end', function() { + callback(); + }); + + }, + + function( callback ) { + + var q = client.query("DROP TYPE request_state"); + + q.on('end', function() { + callback(); + }); + + } + + ], function( err, results ) { + + if( ! err ) next(); + + }); + + }); + +}; diff --git a/models/requests.js b/models/requests.js new file mode 100644 index 0000000..19b0361 --- /dev/null +++ b/models/requests.js @@ -0,0 +1,65 @@ +var pg = require('pg'); +var error = require('../routes/errorHandler'); + +/* + * Ajout d'une nouvelle demande dans la base de données + */ +exports.add = function( data, next, callback ) { + + pg.connect(process.env.DATABASE_URL, function( err, client, done ) { + + client.query('INSERT INTO public.requests( reference, mail, date, state ) \ + VALUES( $1, $2, DEFAULT, DEFAULT)', + + [ data.reference, data.mail ], function( err, result ) { + + if( error.handler( err, client, done, next ) ) return; + done(); + + if( result.rowCount == 1 ) + callback( true ); + else + callback( false ); + + }); + }); + +}; + +/* + * Permet de récupérer l'ensemble des demandes en attentes ( pending ) + */ +exports.getPendingRequests = function( next, callback ) { + + pg.connect(process.env.DATABASE_URL, function( err, client, done ) { + client.query("SELECT r.id, r.reference, r.mail, s.type, s.name \ + FROM public.requests r \ + LEFT JOIN public.servers s ON s.reference = r.reference \ + WHERE r.state = $1", + [ 'pending' ], function( err, result ) { + + if( error.handler( err, client, done, next ) ) return; + done(); + + callback( result.rows ); + + }); + }); + +}; + +/* + * Mise à jour de l'état de la demande ( pending -> done ) + */ +exports.updateState = function( requestId, next ) { + + pg.connect(process.env.DATABASE_URL, function( err, client, done ) { + client.query('UPDATE public.requests SET state = $1 WHERE id = $2', [ 'done', requestId ], function( err, result ) { + + if( error.handler( err, client, done, next ) ) return; + done(); + + }); + }); + +}; diff --git a/models/servers.js b/models/servers.js new file mode 100644 index 0000000..13cd03f --- /dev/null +++ b/models/servers.js @@ -0,0 +1,56 @@ +var pg = require('pg'); +var async = require('async'); +var error = require('../routes/errorHandler'); + +/* + * Permet de récupérer l'ensemble des offres d'OVH par gamme ( sys/kimsufi ) + */ +exports.getServers = function( type, next, callback ) { + + pg.connect(process.env.DATABASE_URL, function( err, client, done ) { + client.query("SELECT * FROM public.servers WHERE type = $1", [ type ], function( err, result ) { + + if( error.handler( err, client, done, next ) ) return; + done(); + + callback( result.rows ); + + }); + }); + +}; + +/* + * Permet de récupérer l'ensemble des références des serveurs d'OVH + */ +exports.getAllRefs = function( next, callback ) { + + pg.connect(process.env.DATABASE_URL, function( err, client, done ) { + client.query("SELECT reference FROM public.servers", function( err, result ) { + + if( error.handler( err, client, done, next ) ) return; + done(); + + var refsList = []; + + async.each(result.rows, function( row, nextRow ) { + + refsList.push( row.reference ); + nextRow(); + + }, function( err ) { + + if( err ) { + next( err ); + return; + } + + callback( refsList ); + + }); + + }); + }); + +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f7c6ca --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "ovh-availability", + "version": "1.0.0", + "description": "Application permettant de connaitre la disponibilité des serveurs d'OVH (SoYourStart, Kimsufi)", + "main": "web.js", + "scripts": { + "start": "node web.js", + "postinstall": "./node_modules/bower/bin/bower -s install" + }, + "dependencies": { + "async": "0.x", + "body-parser": "1.9.x", + "bower": "1.3.x", + "compression": "1.1.x", + "cookie-parser": "1.3.x", + "csurf": "1.6.x", + "db-migrate": "0.8.x", + "debug": "2.x", + "errorhandler": "1.2.x", + "express": "4.9.x", + "express-session": "1.8.x", + "express-validator": "2.6.x", + "jade": "1.x", + "migrate": "^0.1.6", + "morgan": "1.3.x", + "optimist": "0.6.x", + "pg": "3.4.x", + "request": "2.x", + "sendgrid": "1.2.x", + "string": "2.1.x" + }, + "devDependencies": { + "express-debug": "1.1.x" + }, + "engines": { + "node": "0.10.x" + }, + "repository": { + "type": "git", + "url": "https://github.com/hardware/ovh-availability.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/hardware/ovh-availability/issues" + }, + "homepage": "https://github.com/hardware/ovh-availability" +} diff --git a/public/css/template.css b/public/css/template.css new file mode 100644 index 0000000..30a7f15 --- /dev/null +++ b/public/css/template.css @@ -0,0 +1,103 @@ +body { + padding-top: 20px; + padding-bottom: 20px; +} + +pre { + font-size:10px; + white-space:pre; + color:firebrick; + margin-bottom:20px; +} + +input { + margin-right: 5px; +} + +.header, +.footer { + padding-right: 15px; + padding-left: 15px; +} + +.footer { + padding-top: 19px; + color: #777; + border-top: 1px solid #e5e5e5; +} + +.btn-group.space-right { + margin-right:10px; +} + +.bs-callout { + padding: 20px; + margin: 20px 0px; + border-width: 1px 1px 1px 5px; + border-style: solid; + border-color: #EEE; + -moz-border-top-colors: none; + -moz-border-right-colors: none; + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + border-image: none; + border-radius: 3px; +} + +.bs-callout-info { + border-left-color: #5BC0DE; +} + +.bs-callout-danger { + border-left-color: #D9534F; +} + +.bs-callout-warning { + border-left-color: #F0AD4E; +} + +.bs-callout h4 { + margin-top: 0; + margin-bottom: 10px; +} + +.bs-callout-info h4 { + color: #5bc0de; +} + +.align-right { + text-align: right; +} + +.align-center { + text-align: center; +} + +span.help-block { + color: #A94442; +} + + +a.nostyle { + text-decoration:none; +} + +@media (min-width: 768px) { + .container { + max-width: 820px; + } +} + +@media screen and (min-width: 768px) { + + .header, + .footer { + padding-right: 0; + padding-left: 0; + } + + .header { + margin-bottom: 30px; + } + +} diff --git a/public/img/.gitkeep b/public/img/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/js/.gitkeep b/public/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/routes/cron.js b/routes/cron.js new file mode 100644 index 0000000..fde6b7c --- /dev/null +++ b/routes/cron.js @@ -0,0 +1,132 @@ +var async = require('async'); +var request = require('request'); +var mailer = require('./mailer'); +var requestModel = require('../models/requests'); + +/* + * Traitement de l'ensemble des demandes en attentes + * Route : /cron/handleRequests/:secureKey + * Methode : GET + */ +exports.handleRequests = function( req, res, next ) { + + checkSecureKey(req, res, req.params.secureKey, function() { + requestModel.getPendingRequests(next, function( pendingRequests ) { + request(process.env.OVH_API_URL, function( error, response, body ) { + + if( ! error && response.statusCode == 200 ) { + + var data = JSON.parse( body ); + + async.each(pendingRequests, function( request, nextRequest ) { + async.each(data.answer.availability, function( offer, nextOffer ) { + + if( offer.reference == request.reference ) { + + var availableZones = 0 + + async.each(offer.zones, function( zone, nextZone ) { + + if( zone.availability != 'unknown' && zone.availability != 'unavailable' ) + availableZones++; + + nextZone(); + + }, function( err ) { + + if( err ) { next( err ); return; } + + if( availableZones > 0 ) + inform( request, next ); + + }); + + } + + nextOffer(); + + }, function( err ) { + + if( err ) { next( err ); return; } + + nextRequest(); + + }); + + }, function( err ) { + + if( err ) { next( err ); return; } + + res.send('PROCESSING REQUESTS COMPLETED !'); + + }); + + } + + }); + }); + }); + +}; + +/* + * Permet d'informer l'utilisateur par mail de la disponibilité d'un offre d'OVH + */ +var inform = function( request, next ) { + + var orderUrl = '' + + switch( request.type ) { + case 'sys': + orderUrl = 'https://eu.soyoustart.com/fr/commande/soYouStart.xml?reference=' + request.reference + break; + case 'kimsufi': + orderUrl = 'https://www.kimsufi.com/fr/commande/kimsufi.xml?reference=' + request.reference + break; + } + + var payload = { + to:request.mail, + from:'nepasrepondre@ovh-availability', + subject:'[ovh-availability] Votre serveur est disponible ( ' + request.name + ' )', + html:"

Bonjour,

\ +

Le serveur " + request.name + " est disponible.

\ +

Pour le réserver, cliquez sur le lien ci-dessous :

\ + Commander \ +

Si vous avez raté l'offre, vous pouvez toujours refaire une demande via :

\ + " + process.env.APP_URL + " \ +

A très bientôt sur ovh-availability

" + }; + + mailer.send( payload, next ); + + // Mise à jour de l'état de la demande ( pending -> done ) + requestModel.updateState( request.id, next ); + +} + +/* + * Vérification de la clé sécurisée + */ +var checkSecureKey = function( req, res, secureKey, callback ) { + + if( secureKey === process.env.CRON_KEY ) + callback(); + else + res.send('invalid secure key ! :('); + +} + +/* + ####### Heroku scheduler ####### + + COMMAND FREQUENCY +-------------------------------------------- +| cron --task handleRequests | every 10min | +-------------------------------------------- + +Note : + - pour la commande cron voir le fichier bin/cron + - pour paramétrer le scheduler : heroku addons:open scheduler + +*/ diff --git a/routes/data.js b/routes/data.js new file mode 100644 index 0000000..58d5e97 --- /dev/null +++ b/routes/data.js @@ -0,0 +1,43 @@ +exports.settings = function( req, res, options, callback ) { + + var isLogged = ( req.session.user ) ? true : false; + + // Contrôle de la session dans les zones restreintes + if( !! options.shouldBeLogged && ! isLogged ) { + res.redirect('/user/login?url=' + req.url); + return; + } + + if( ! options.shouldBeLogged && isLogged ) { + + if( ! options.mayBeLogged ) { + res.redirect('/account'); + return; + } + + } + + var settings = { + path:req.path, + title:"OVH Disponibilité", + isLogged:isLogged + }; + + if( isLogged ) { + + var isAdmin = ( req.session.user.type === 'admin' ) ? true : false; + + settings.user = req.session.user; + settings.isAdmin = isAdmin; + + // Contrôle de la session dans les zones restreintes + if( !! options.shouldBeAdmin && ! isAdmin ) { + res.redirect('/account'); + return; + } + + } + + callback( settings ); + +}; diff --git a/routes/errorHandler.js b/routes/errorHandler.js new file mode 100644 index 0000000..e5b6185 --- /dev/null +++ b/routes/errorHandler.js @@ -0,0 +1,11 @@ +exports.handler = function( err, client, done, next ) { + + if( ! err ) return false; + + err.status = 500; + done( client ); + next( err ); + + return true; + +}; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..d7b5de5 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,132 @@ +var async = require('async'); +var data = require('./data'); +var recaptcha = require('./recaptcha'); +var serversModel = require('../models/servers'); +var requestModel = require('../models/requests'); + +/* + * INDEX + * Route : / + * Methode : GET + */ +exports.index = function( req, res, next ) { + + data.settings(req, res, { shouldBeLogged:false, mayBeLogged:true }, function( settings ) { + + serversModel.getServers('sys', next, function( sysServersList ) { + serversModel.getServers('kimsufi', next, function( kimServersList ) { + + settings.formErrors = {}; + settings.sysServersList = sysServersList; + settings.kimServersList = kimServersList; + + res.render('index', settings); + + }); + }); + + }); + +}; + +/* + * INDEX + * Route : / + * Methode : POST + */ +exports.run = function( req, res, next ) { + + data.settings(req, res, { shouldBeLogged:false, mayBeLogged:true }, function( settings ) { + + // Récupération des ressources + serversModel.getServers('sys', next, function( sysServersList ) { + serversModel.getServers('kimsufi', next, function( kimServersList ) { + serversModel.getAllRefs(next, function( refsList ) { + + // Validation des valeurs contenues dans req.body + req.checkBody('mail', 'La valeur de ce champ est invalide.').isEmail().len(5, 100); + req.checkBody('mail', 'Ce champ est requis.').notEmpty(); + req.checkBody('server', 'La valeur de ce champ est invalide.').isIn( refsList ); + req.checkBody('server', 'Ce champ est requis.').notEmpty(); + + var errors = req.validationErrors( true ); + + async.waterfall([ + + // Vérification du formulaire + function( callback ) { + + if( errors ) + callback("Une erreur est survenue lors de la validation du formulaire, veuillez vérifier les données saisies."); + else + callback(); + + }, + + // Vérification du captcha + function( callback ) { + + recaptcha.verify(req, req.body["g-recaptcha-response"], next, function( result ) { + + if( ! result ) + callback("Veuillez cocher la case située à la fin du formulaire afin de prouver que vous êtes bien humain."); + else + callback(); + + }); + + }, + + // Ajout de la demande au sein de la base de données + function( callback ) { + + var data = { + reference:req.body.server, + mail:req.body.mail + }; + + requestModel.add(data, next, function( result ) { + + if( result ) { + + callback(); + + } else { + + callback('Une erreur est survenue lors de l\'enregistrement de votre demande dans la base de données.'); + + } + + }); + + } + + ], function( err, result ) { + + if( err ) { + + settings.formError = true; + settings.formMessage = err; + + } else { + + settings.formSuccess = true; + settings.formMessage = 'Votre demande a bien été prise en compte.'; + + } + + settings.formErrors = ( errors ) ? errors : {}; + settings.sysServersList = sysServersList; + settings.kimServersList = kimServersList; + + res.render('index', settings); + + }); + + }); + }); + }); + + }); + +}; diff --git a/routes/mailer.js b/routes/mailer.js new file mode 100644 index 0000000..2854cb9 --- /dev/null +++ b/routes/mailer.js @@ -0,0 +1,106 @@ +var async = require('async'); +var sendgrid = require('sendgrid')( + process.env.SENDGRID_USERNAME, + process.env.SENDGRID_PASSWORD +); + +exports.send = function( email, next ) { + + async.waterfall([ + function( callback ) { + + var fullBody = ''; + + var prefix = getPrefix(); + var suffix = getSuffix(); + var body = email.html; + + fullBody += prefix; + fullBody += body; + fullBody += suffix; + + callback( null, fullBody ); + }, + function( fullBody, callback ) { + + email.html = fullBody; + + callback( null, email ); + } + ], + + function( err, email ) { + + if( err ) { + + next( err ); + return; + + } else { + + sendgrid.send(email, function( err, json ) { + + if( err ) + next(new Error("Une erreur est survenue pendant l'exécution du module de mail [ " + err + " ]")); + + }); + + } + + }); + +}; + +var getPrefix = function() { + + var html = ' \ + \ + \ + \ + \ + \ +
\ + \ + \ +
\ + \ + \ +
\ + \ + \ +
'; + + return html; +}; + +var getSuffix = function( callback ) { + + var html = '
\ + '; + + return html; + +}; diff --git a/routes/recaptcha.js b/routes/recaptcha.js new file mode 100644 index 0000000..f79f5e9 --- /dev/null +++ b/routes/recaptcha.js @@ -0,0 +1,32 @@ +var request = require('request') + +exports.verify = function( req, response, next, callback ) { + + var ip = req.headers['x-forwarded-for'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket.remoteAddress; + + var url = "https://www.google.com/recaptcha/api/siteverify?secret=" + process.env.RECAPTCHA_PRIVATE_KEY + "&response=" + response + "&remoteip=" + ip + + request(url, function( error, response, body ) { + + if ( error || response.statusCode != 200 ) { + + next( error ); + return; + + } else { + + var result = JSON.parse( body ); + + if( result.success ) + callback( true ); + else + callback( false ); + + } + + }); + +}; diff --git a/views/error.jade b/views/error.jade new file mode 100644 index 0000000..95fddc0 --- /dev/null +++ b/views/error.jade @@ -0,0 +1,8 @@ +extends layout + +block content + hr + .alert.alert-danger!= 'ERREUR ' + ( ( error.status ) ? error.status : '' ) + ' : ' + error.message + + if error.stack + pre= '[ STACK TRACE ] ' + error.stack diff --git a/views/includes/footer.jade b/views/includes/footer.jade new file mode 100644 index 0000000..a20b551 --- /dev/null +++ b/views/includes/footer.jade @@ -0,0 +1,4 @@ +.footer + p + | © OVH-AVAILABILITY 2014 + | v1.0.0 diff --git a/views/includes/header.jade b/views/includes/header.jade new file mode 100644 index 0000000..bae291f --- /dev/null +++ b/views/includes/header.jade @@ -0,0 +1 @@ +.header diff --git a/views/includes/javascript.jade b/views/includes/javascript.jade new file mode 100644 index 0000000..922f758 --- /dev/null +++ b/views/includes/javascript.jade @@ -0,0 +1 @@ +script(src='/bower/bootstrap/dist/js/bootstrap.min.js') diff --git a/views/index.jade b/views/index.jade new file mode 100644 index 0000000..49f10ba --- /dev/null +++ b/views/index.jade @@ -0,0 +1,50 @@ +extends layout + +block content + + div(class='bs-callout bs-callout-info' id='info-top') + h4.align-center Disponibilité des offres OVH + hr + p + | Pour connaitre la disponibilité des offres d'OVH (SoYourStart / Kimsufi), choisissez un serveur parmis la liste ci-dessous et ajoutez votre adresse mail. + | Vous serez prévenu par email lorsque le serveur souhaité sera disponible. + + if formSuccess + .alert.alert-success !{formMessage} + else + + if formError + .alert.alert-danger #{formMessage} + + form(action='/' method='post' class='form-horizontal' role='form') + + input(type='hidden' name='_csrf' value=token) + + .form-group + label(for='server' class='col-sm-4 control-label') Liste des serveurs : + .col-sm-5 + select(class='form-control' name='server') + option(value='default') Choisir... + optgroup(label='Gamme SoYouStart :') + for server in sysServersList + option(value=server.reference) #{server.name} + optgroup(label='Gamme Kimsufi :') + for server in kimServersList + option(value=server.reference) #{server.name} + if formErrors["server"] + span.help-block #{formErrors["server"]["msg"]} + + .form-group + label(for='mail' class='col-sm-4 control-label') Email : + .col-sm-5 + input(type='mail' name='mail' class='form-control' id='mail' placeholder='Votre adresse email') + if formErrors["mail"] + span.help-block #{formErrors["mail"]["msg"]} + + div(class='form-group' id='recaptcha') + .col-sm-offset-4.col-sm-10 + div(class='g-recaptcha' data-sitekey=recaptchaKey) + + .form-group + .col-sm-offset-4.col-sm-10 + button(type='submit' class='btn btn-success') Valider diff --git a/views/layout.jade b/views/layout.jade new file mode 100644 index 0000000..3937a75 --- /dev/null +++ b/views/layout.jade @@ -0,0 +1,33 @@ +mixin ie(condition) + | + +doctype html +html + head + meta(charset='utf-8') + meta(http-equiv='X-UA-Compatible' content='IE=edge') + meta(name='viewport' content='width=device-width, initial-scale=1') + title=title + + link(href='/bower/bootstrap/dist/css/bootstrap.min.css', rel='stylesheet') + link(href='/css/template.css', rel='stylesheet') + + +ie('if lt IE 9') + script(src='/bower/html5shiv/dist/html5shiv.js') + script(src='/bower/respond/dest/respond.min.js') + + script(src='/bower/jquery/dist/jquery.min.js') + script(src='https://www.google.com/recaptcha/api.js' async defer) + + body + .container + include includes/header + + .content + block content + + include includes/footer + + include includes/javascript diff --git a/web.js b/web.js new file mode 100644 index 0000000..7ace371 --- /dev/null +++ b/web.js @@ -0,0 +1,83 @@ +var express = require('express'); +var http = require('http'); +var path = require('path'); +var logger = require('morgan'); +var compression = require('compression'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); +var session = require('express-session'); +var validator = require('express-validator'); +var csrf = require('csurf'); +var errorHandler = require('errorhandler'); + +var routes = require('./routes'); +var cron = require('./routes/cron'); + +var app = express(); + +// URL de la base de donnée ( prod / dev ) +process.env.DATABASE_URL = process.env.DATABASE_URL || 'tcp://localhost:5432/ovh-availability'; + +app.set('env', process.env.ENV || 'development'); +app.set('port', process.env.PORT); +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +if(app.get('env') == 'development') { + app.use(logger('dev')); + var edt = require('express-debug'); + edt(app); +} + +app.use(compression()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(validator()); +app.use(cookieParser(process.env.COOKIES_SECRET)); +app.use(session({ secret: process.env.SESSION_SECRET, key: 'SID', resave:true, saveUninitialized:true })); +app.use(csrf()); + +app.use(function( req, res, next ) { + res.locals.token = req.csrfToken(); + res.locals.recaptchaKey = process.env.RECAPTCHA_PUBLIC_KEY; + next(); +}); + +app.use(express.static(path.join(__dirname, 'public'))); + + +/* + * ROUTES + */ + + +// INDEX +app.get('/', routes.index); +app.post('/', routes.run); + +// CRON +app.get('/cron/handleRequests/:secureKey', cron.handleRequests); + +if(app.get('env') == 'development') { + app.use(errorHandler()); +} + +/* + * ERREUR 404 + */ +app.use(function( req, res, next ) { + var err = new Error('Page introuvable'); + err.status = 404; + next( err ); +}); + +/* + * TOUTES LES AUTRES ERREURS + */ +app.use(function( err, req, res, next ) { + res.render('error', { title:'Erreur', error:err }); +}); + +var server = app.listen(app.get('port'), function() { + console.log('Express server listening on port %d', app.get('port')); +});