├── nodemon.json ├── routes ├── doc.js ├── auth.js ├── stats.js ├── log.js ├── checkin.js ├── index.js ├── review.js ├── revision.js ├── datasource.js ├── tag.js ├── local.js └── user.js ├── views ├── error.html ├── tag │ ├── post.html │ ├── delete.html │ ├── get_id.html │ ├── get.html │ └── put.html ├── user │ ├── delete_all.html │ ├── delete.html │ ├── get.html │ ├── get_id.html │ ├── post.html │ └── put.html ├── revision │ ├── post.html │ ├── delete.html │ ├── put.html │ ├── get.html │ └── get_id.html ├── local │ ├── delete.html │ ├── get_light.html │ ├── get.html │ ├── get_id.html │ ├── post.html │ └── put.html ├── review │ ├── delete.html │ ├── post.html │ ├── get.html │ └── get_id.html ├── auth │ └── token.html ├── checkin │ ├── get.html │ ├── get_id.html │ └── post.html ├── layout.html ├── log │ └── get_id.html └── index.html ├── models ├── datasource.js ├── revision.js ├── checkin.js ├── tag.js ├── log.js ├── review.js ├── index.js ├── local.js └── user.js ├── src ├── javascripts │ └── main.js └── stylesheets │ ├── api.css │ └── normaset.css ├── bin └── www ├── config ├── config.json └── acl.json ├── controllers ├── StatsController.js ├── LogController.js ├── CheckinController.js ├── TagController.js ├── RevisionController.js ├── DataSourceController.js ├── ReviewController.js ├── AuthController.js ├── UserController.js └── LocalController.js ├── .gitignore ├── migrations ├── 20180206012509-create-data-source.js ├── 20170901010554-AddGoogleIdFieldToUser.js ├── 20161208165149-ColumnRole.js ├── 20161228195938-ColumnMethod.js ├── 20161228203042-ColumnIpOrigin.js ├── 20180205030357-AddIsPaidToLocal.js ├── 20170116180926-ColumnAuthorIP.js ├── 20170816041815-AddColumnUserIdToReview.js ├── 20170903052706-AddIsCoveredToLocal.js ├── 20180205030240-AddSlotsToLocal.js ├── 20170812221223-AddColumnUserIDToReview.js ├── 20180206014552-AddSourceIdToLocal.js ├── 20171120192818-AddViewsToLocal.js ├── 20161127140133-unnamed-migration.js ├── 20170811230516-CreateColumnsEmailAndFacebookID.js ├── 20170812005551-ChangeNotNullColumnsForUsers.js └── 20171213000916-AddCityStateToLocal.js ├── tests └── unit │ └── controllers │ ├── TagController_test.js │ └── AuthController_test.js ├── .travis.yml ├── README.md ├── gulpfile.js ├── LICENSE ├── package.json ├── app.js └── public └── stylesheets └── all.min.css /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "*.md", 6 | "coverage", 7 | "node_modules/*", 8 | "public/", 9 | "src/*" 10 | ], 11 | "verbose": true, 12 | "ext": "js html" 13 | } 14 | -------------------------------------------------------------------------------- /routes/doc.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | 4 | router.get('/', function (request, response) { 5 | response.render('index', {title: 'bike de boa API'}) 6 | }) 7 | 8 | module.exports = router 9 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{{ message }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |

{{ message }}

9 |

{{ message }}

10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let models = require('../models') 4 | let AuthController = require('../controllers/AuthController')(models.User) 5 | 6 | router.post('/', AuthController.token.bind(AuthController)) 7 | 8 | module.exports = router 9 | -------------------------------------------------------------------------------- /views/tag/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /tag 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |       "name": "Iluminado"
    11 |     }
    12 |     
    13 |
    14 |
  • 15 | -------------------------------------------------------------------------------- /routes/stats.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let AuthController = require('../controllers/AuthController')(models.User) 6 | let StatsController = require('../controllers/StatsController')() 7 | 8 | router.get('/', StatsController.getAll.bind(StatsController)) 9 | 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /views/user/delete_all.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /user 5 |
    6 |
    7 |

    Response Body

    8 |
     9 |     {
    10 |       "message": "Deleted successfully"
    11 |     }
    12 |     
    13 |
    14 |
  • 15 | -------------------------------------------------------------------------------- /views/revision/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /revision 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |       "local_id": 10,
    11 |       "comments": "Message for revision"
    12 |     }
    13 |     
    14 |
    15 |
  • 16 | -------------------------------------------------------------------------------- /models/datasource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var DataSource = sequelize.define('DataSource', { 4 | name: DataTypes.STRING, 5 | url: DataTypes.STRING 6 | }, { 7 | timestamps: true, 8 | freezeTableName: true, 9 | classMethods: { 10 | associate: function(models) { 11 | DataSource.hasMany(models.Local, { foreignKey: 'datasource_id', hooks: false }); 12 | } 13 | } 14 | }); 15 | return DataSource; 16 | }; -------------------------------------------------------------------------------- /routes/log.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let LogController = require('../controllers/LogController')(models.Log) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/:_page?', LogController.getAll.bind(LogController)) 12 | 13 | module.exports = router 14 | -------------------------------------------------------------------------------- /views/tag/delete.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /tag/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Tag's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "message": "Deleted successfully"
    14 |     }
    15 |     
    16 |
    17 |
  • 18 | -------------------------------------------------------------------------------- /views/local/delete.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /local/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Local's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "message": "Deleted successfully"
    14 |     }
    15 |     
    16 |
    17 |
  • 18 | -------------------------------------------------------------------------------- /views/user/delete.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /user/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | User's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "message": "Deleted successfully"
    14 |     }
    15 |     
    16 |
    17 |
  • 18 | -------------------------------------------------------------------------------- /views/review/delete.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /review/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Review's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "message": "Deleted successfully"
    14 |     }
    15 |     
    16 |
    17 |
  • 18 | -------------------------------------------------------------------------------- /views/revision/delete.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | DELETE 4 | /revision/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Revision's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "message": "Deleted successfully"
    14 |     }
    15 |     
    16 |
    17 |
  • 18 | -------------------------------------------------------------------------------- /src/javascripts/main.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 'use strict'; 3 | 4 | // var $endpointsLists = $('ul.endpoints'); 5 | // $endpointsLists.hide(); 6 | 7 | // $('.api-item-title').on('click', function(event) { 8 | // var $apiItem = $(this).parent('.api-item').find('ul.endpoints'); 9 | // $endpointsLists.not($apiItem).hide("fast"); 10 | 11 | // if ($apiItem.is(':visible')) { 12 | // $apiItem.hide("fast"); 13 | // } else { 14 | // $apiItem.show("fast"); 15 | // } 16 | // }); 17 | 18 | }(jQuery)); 19 | -------------------------------------------------------------------------------- /views/auth/token.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /token 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |       username: '',
    11 |       password: ''
    12 |     }
    13 |     
    14 | 15 |

    Response Body

    16 |
    17 |     {
    18 |       token: ''
    19 |     }
    20 |     
    21 |
    22 |
  • 23 | -------------------------------------------------------------------------------- /views/user/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /user 5 |
    6 |
    7 |

    Response Body

    8 |
     9 | [
    10 |   {
    11 |     "id": 1,
    12 |     "fullname": "John",
    13 |     "username": "john",
    14 |     "createdAt": "2016-11-07T00:19:04.459Z",
    15 |     "updatedAt": "2016-11-07T00:19:04.459Z"
    16 |   }
    17 | ]
    18 | 
    19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /views/tag/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /tag/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Tag's _id

    9 | 10 |

    Response Body

    11 |
    12 | {
    13 |   "id": 1,
    14 |   "name": "Iluminado",
    15 |   "createdAt": "2016-11-08T14:56:40.207Z",
    16 |   "updatedAt": "2016-11-08T14:56:40.207Z"
    17 | }
    18 | 
    19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('dotenv').config(); 3 | let app = require('../app') 4 | let debug = require('debug')('api:server') 5 | let models = require('../models') 6 | let PORT = process.env.PORT || 3000 7 | 8 | models.sequelize.sync({logging: false}).then(function () { 9 | debug('Sequelize -> Synchronized') 10 | }); 11 | 12 | let server = app.listen(PORT, function () { 13 | let host = server.address().address 14 | let port = server.address().port 15 | debug('NODE_ENV = ' + process.env.NODE_ENV) 16 | debug('APP rodando em http://%s:%s', host, port) 17 | }); 18 | -------------------------------------------------------------------------------- /views/checkin/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /checkin 5 |
    6 |
    7 |

    Response Body

    8 |
     9 | [
    10 |   {
    11 |     "id": 1,
    12 |     "date": "2016-11-30T00:00:00.000Z",
    13 |     "hour": "10:45:56",
    14 |     "createdAt": "2016-11-30T12:45:56.046Z",
    15 |     "updatedAt": "2016-11-30T12:45:56.046Z",
    16 |     "local_id": 1
    17 |   }
    18 | ]
    19 | 
    20 |
    21 |
  • 22 | -------------------------------------------------------------------------------- /views/user/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /user/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | User's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "id": 1,
    14 |       "fullname": "John",
    15 |       "username": "john",
    16 |       "createdAt": "2016-11-07T00:19:04.459Z",
    17 |       "updatedAt": "2016-11-07T00:19:04.459Z"
    18 |     }
    19 |     
    20 |
    21 |
  • 22 | -------------------------------------------------------------------------------- /views/checkin/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /checkin/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Checkin's _id

    9 | 10 |

    Response Body

    11 |
    12 | {
    13 |   "id": 1,
    14 |   "date": "2016-11-30T00:00:00.000Z",
    15 |   "hour": "10:45:56",
    16 |   "createdAt": "2016-11-30T12:45:56.046Z",
    17 |   "updatedAt": "2016-11-30T12:45:56.046Z",
    18 |   "local_id": 1
    19 | }
    20 | 
    21 |
    22 |
  • 23 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "bikedeboa", 4 | "password": "bikedeboa2016", 5 | "database": "bikedeboa", 6 | "host": "bikedeboa.postgresql.dbaas.com.br", 7 | "dialect": "postgres" 8 | }, 9 | "test": { 10 | "username": "bikedeboa_test", 11 | "password": "bikedeboa2016", 12 | "database": "bikedeboa_test", 13 | "host": "bikedeboa_test.postgresql.dbaas.com.br", 14 | "dialect": "postgres" 15 | }, 16 | "production": { 17 | "use_env_variable": "DATABASE_URL", 18 | "dialect": "postgres", 19 | "ssl": true, 20 | "dialectOptions": { 21 | "ssl": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /views/tag/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /tag 5 |
    6 |
    7 |

    Response Body

    8 |
     9 | [
    10 |   {
    11 |     "id": 1,
    12 |     "name": "Iluminado",
    13 |     "createdAt": "2016-11-08T14:56:40.207Z",
    14 |     "updatedAt": "2016-11-08T14:56:40.207Z"
    15 |   },
    16 |   {
    17 |     "id": 2,
    18 |     "name": "Movimentado",
    19 |     "createdAt": "2016-11-08T14:57:19.407Z",
    20 |     "updatedAt": "2016-11-08T14:57:51.586Z"
    21 |   }
    22 | ]
    23 | 
    24 |
    25 |
  • 26 | -------------------------------------------------------------------------------- /views/user/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /user 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |       "fullname": "John",
    11 |       "username": "john",
    12 |       "password": "#######",
    13 |       "role": "colaborator"
    14 |     }
    15 |     
    16 | 17 |

    Response Body

    18 |
    19 |     {
    20 |       "fullname": "John",
    21 |       "username": "john",
    22 |       "role": "colaborator"
    23 |     }
    24 |     
    25 |
    26 |
  • 27 | -------------------------------------------------------------------------------- /routes/checkin.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let CheckinController = require('../controllers/CheckinController')(models.Checkin) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/', CheckinController.getAll.bind(CheckinController)) 12 | router.get('/:_id', CheckinController.getById.bind(CheckinController)) 13 | router.post('/', AuthController.middlewareLogging, CheckinController.create.bind(CheckinController)) 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /views/user/put.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | PUT 4 | /user/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Users's _id

    9 | 10 |

    Request Body

    11 |
    12 |     {
    13 |       "fullname": "John",
    14 |       "username": "john",
    15 |       "password": "#######"
    16 |     }
    17 |     
    18 | 19 |

    Response Body

    20 |
    21 |     {
    22 |       "fullname": "John",
    23 |       "username": "john"
    24 |     }
    25 |     
    26 |
    27 |
  • 28 | -------------------------------------------------------------------------------- /views/checkin/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /checkin 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |      {
    10 |       "local_id": 1
    11 |     }
    12 |     
    13 | 14 |

    Response Body

    15 |
    16 |     {
    17 |       "id": 1,
    18 |       "date": "2016-11-30T00:00:00.000Z",
    19 |       "hour": "10:45:56",
    20 |       "createdAt": "2016-11-30T12:45:56.046Z",
    21 |       "updatedAt": "2016-11-30T12:45:56.046Z",
    22 |       "local_id": 1
    23 |     }
    24 |     
    25 |
    26 |
  • 27 | -------------------------------------------------------------------------------- /views/tag/put.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | PUT 4 | /tag/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Tag's _id

    9 | 10 |

    Request Body

    11 |
    12 |     {
    13 |       "name": "Iluminado"
    14 |     }
    15 |     
    16 | 17 |

    Response Body

    18 |
    19 |     {
    20 |       "id": 1,
    21 |       "name": "Iluminado",
    22 |       "createdAt": "2016-11-08T14:56:40.207Z",
    23 |       "updatedAt": "2016-11-08T14:56:40.207Z"
    24 |     }
    25 |     
    26 |
    27 |
  • 28 | -------------------------------------------------------------------------------- /models/revision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Revision = sequelize.define('Revision', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | comments: { 10 | type: DataTypes.STRING 11 | } 12 | }, { 13 | timestamps: true, 14 | freezeTableName: true, 15 | classMethods: { 16 | associate: function(models) { 17 | // associations can be defined here 18 | Revision.belongsTo(models.Local, {foreignKey: 'local_id', hooks: true}); 19 | } 20 | } 21 | }); 22 | return Revision; 23 | }; 24 | -------------------------------------------------------------------------------- /views/local/get_light.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /local/light 5 |
    6 |
    7 |

    Response Body

    8 |
     9 |     [
    10 |       {
    11 |         "average": "4.0000000000000000"
    12 |         "id": 931
    13 |         "isPublic": true
    14 |         "lat": "-30.037464"
    15 |         "lng": "-51.212618"
    16 |         "photo": "https://s3.amazonaws.com/bikedeboa/images/931.jpeg"
    17 |         "reviews": "1"
    18 |         "structureType": "deroda"
    19 |         "text": "Academia da Redenção"
    20 |       }
    21 |     ]
    22 |     
    23 |
    24 |
  • 25 | -------------------------------------------------------------------------------- /models/checkin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Checkin = sequelize.define('Checkin', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | date: { 10 | type: DataTypes.DATE, 11 | allowNull: false 12 | }, 13 | hour: { 14 | type: DataTypes.TIME, 15 | allowNull: false 16 | } 17 | }, { 18 | timestamps: true, 19 | freezeTableName: true, 20 | classMethods: { 21 | associate: function(models) { 22 | // associations can be defined here 23 | } 24 | } 25 | }); 26 | return Checkin; 27 | }; -------------------------------------------------------------------------------- /controllers/StatsController.js: -------------------------------------------------------------------------------- 1 | let moment = require('moment') 2 | let models = require('../models') 3 | 4 | function StatsController () { 5 | } 6 | 7 | StatsController.prototype.getAll = function (request, response, next) { 8 | models.Local.count().then(localsCount => { 9 | models.Review.count().then(reviewsCount => { 10 | models.Local.sum('views').then(viewsCount => { 11 | response.json({ 12 | localsCount: localsCount, 13 | reviewsCount: reviewsCount, 14 | viewsCount: viewsCount 15 | }) 16 | }) 17 | .catch(next) 18 | }) 19 | .catch(next) 20 | }) 21 | .catch(next) 22 | } 23 | 24 | module.exports = function () { 25 | return new StatsController() 26 | } 27 | -------------------------------------------------------------------------------- /views/revision/put.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | PUT 4 | /revision/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Revision's _id

    9 | 10 |

    Request Body

    11 |
    12 |     {
    13 |       "comments": "Message for revision"
    14 |     }
    15 |     
    16 | 17 |

    Response Body

    18 |
    19 |     {
    20 |       "id": 1,
    21 |       "local_id": 10,
    22 |       "comments": "Message for revision",
    23 |       "createdAt": "2016-11-08T14:56:40.207Z",
    24 |       "updatedAt": "2016-11-08T14:56:40.207Z"
    25 |     }
    26 |     
    27 |
    28 |
  • 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | npm-debug.log* 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | *.pid.lock 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Other files 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | 4 | // authentication 5 | router.use('/token', require('./auth')) 6 | // log 7 | router.use('/log', require('./log')) 8 | // user 9 | router.use('/user', require('./user')) 10 | // tag 11 | router.use('/tag', require('./tag')) 12 | // local 13 | router.use('/local', require('./local')) 14 | // checkin 15 | router.use('/checkin', require('./checkin')) 16 | // review 17 | router.use('/review', require('./review')) 18 | // revision 19 | router.use('/revision', require('./revision')) 20 | // datasource 21 | router.use('/datasource', require('./datasource')) 22 | // datasource 23 | router.use('/stats', require('./stats')) 24 | // docs 25 | router.use('/v1/doc', require('./doc')) 26 | 27 | module.exports = router 28 | -------------------------------------------------------------------------------- /views/review/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /review 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |     "rating": 3,
    11 |     "idLocal": 1123,
    12 |       "Tags": [
    13 |         {
    14 |           "id": 1,
    15 |         }
    16 |       ]
    17 |     }
    18 |     
    19 | 20 |

    Response Body

    21 |
    22 |     {
    23 |     "rating": 3,
    24 |     "hour": "16:35:28",
    25 |     "date": "2016-11-26T00:00:00.000Z",
    26 |     "idLocal": 1123,
    27 |       "Tags": [
    28 |         {
    29 |           "id": 1,
    30 |         }
    31 |       ]
    32 |     }
    33 |     
    34 |
    35 |
  • 36 | -------------------------------------------------------------------------------- /migrations/20180206012509-create-data-source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: function(queryInterface, Sequelize) { 4 | return queryInterface.createTable('DataSource', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | type: Sequelize.STRING 13 | }, 14 | url: { 15 | type: Sequelize.STRING 16 | }, 17 | createdAt: { 18 | allowNull: false, 19 | type: Sequelize.DATE 20 | }, 21 | updatedAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | } 25 | }); 26 | }, 27 | down: function(queryInterface, Sequelize) { 28 | return queryInterface.dropTable('DataSource'); 29 | } 30 | }; -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 |
    19 | {% block content %}{% endblock %} 20 |
    21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /migrations/20170901010554-AddGoogleIdFieldToUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | return queryInterface.addColumn( 13 | 'User', 14 | 'google_id', 15 | Sequelize.STRING 16 | ) 17 | }, 18 | 19 | down: function (queryInterface, Sequelize) { 20 | /* 21 | Add reverting commands here. 22 | Return a promise to correctly handle asynchronicity. 23 | 24 | Example: 25 | return queryInterface.dropTable('users'); 26 | */ 27 | return queryInterface.removeColumn('User', 'google_id') 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/unit/controllers/TagController_test.js: -------------------------------------------------------------------------------- 1 | let request = require('supertest') 2 | let app = require('../../../app') 3 | let chai = require('chai') 4 | 5 | describe('Tag', function () { 6 | let token = null 7 | 8 | before(function (done) { 9 | request(app) 10 | .post('/token') 11 | .send({ 12 | username: 'testuser', 13 | password: '123456' 14 | }) 15 | .end(function (err, res) { 16 | token = res.body.token 17 | done() 18 | }) 19 | }) 20 | 21 | it('GET /tag', function (done) { 22 | request(app) 23 | .get('/tag') 24 | .set('Accept', 'application/json') 25 | .set('x-access-token', token) 26 | .expect('Content-Type', /json/) 27 | .expect(200) 28 | .end(function (err, res) { 29 | done(err) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /routes/review.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let ReviewController = require('../controllers/ReviewController')(models.Review) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/', ReviewController.getAll.bind(ReviewController)) 12 | router.get('/:_id', ReviewController.getById.bind(ReviewController)) 13 | router.put('/:_id', ReviewController.update.bind(ReviewController)) 14 | router.delete('/:_id', AuthController.middlewareLogging, ReviewController.remove.bind(ReviewController)) 15 | router.post('/', AuthController.middlewareLogging, ReviewController.create.bind(ReviewController)) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /migrations/20161208165149-ColumnRole.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'User', 14 | 'role', 15 | { 16 | type: Sequelize.STRING 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'User', 31 | 'role' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20161228195938-ColumnMethod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Log', 14 | 'method', 15 | { 16 | type: Sequelize.STRING 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Log', 31 | 'method' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20161228203042-ColumnIpOrigin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Log', 14 | 'ip_origin', 15 | { 16 | type: Sequelize.STRING 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Log', 31 | 'ip_origin' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20180205030357-AddIsPaidToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'isPaid', 15 | { 16 | type: Sequelize.BOOLEAN 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Local', 31 | 'isPaid' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20170116180926-ColumnAuthorIP.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'authorIP', 15 | { 16 | type: Sequelize.STRING 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Local', 31 | 'authorIP' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /models/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Tag = sequelize.define('Tag', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | name: { 10 | type: DataTypes.STRING 11 | } 12 | }, { 13 | timestamps: true, 14 | freezeTableName: true, 15 | classMethods: { 16 | associate: function(models) { 17 | // associations can be defined here 18 | Tag.belongsToMany(models.Local, {through: 'Local_Tags', foreignKey: 'tag_id', otherKey: 'local_id', hooks: true}); 19 | Tag.belongsToMany(models.Review, {through: 'Review_Tags', foreignKey: 'tag_id', otherKey: 'review_id', hooks: true}); 20 | } 21 | } 22 | }); 23 | return Tag; 24 | }; -------------------------------------------------------------------------------- /migrations/20170816041815-AddColumnUserIdToReview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'user_id', 15 | { 16 | type: Sequelize.INTEGER 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Local', 31 | 'user_id' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20170903052706-AddIsCoveredToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'isCovered', 15 | { 16 | type: Sequelize.BOOLEAN 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Local', 31 | 'isCovered' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20180205030240-AddSlotsToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'slots', 15 | { 16 | type: Sequelize.INTEGER 17 | } 18 | ); 19 | 20 | }, 21 | 22 | down: function (queryInterface, Sequelize) { 23 | /* 24 | Add reverting commands here. 25 | Return a promise to correctly handle asynchronicity. 26 | 27 | Example: 28 | return queryInterface.dropTable('users'); 29 | */ 30 | queryInterface.removeColumn( 31 | 'Local', 32 | 'slots' 33 | ); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /migrations/20170812221223-AddColumnUserIDToReview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Review', 14 | 'user_id', 15 | { 16 | type: Sequelize.INTEGER 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Review', 31 | 'user_id' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20180206014552-AddSourceIdToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | queryInterface.addColumn( 13 | 'Local', 14 | 'datasource_id', 15 | { 16 | type: Sequelize.INTEGER 17 | } 18 | ); 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | /* 23 | Add reverting commands here. 24 | Return a promise to correctly handle asynchronicity. 25 | 26 | Example: 27 | return queryInterface.dropTable('users'); 28 | */ 29 | queryInterface.removeColumn( 30 | 'Local', 31 | 'datasource_id' 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/20171120192818-AddViewsToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | 13 | queryInterface.addColumn( 14 | 'Local', 15 | 'views', 16 | { 17 | type: Sequelize.INTEGER, 18 | defaultValue: 0 19 | } 20 | ); 21 | }, 22 | 23 | down: function (queryInterface, Sequelize) { 24 | /* 25 | Add reverting commands here. 26 | Return a promise to correctly handle asynchronicity. 27 | 28 | Example: 29 | return queryInterface.dropTable('users'); 30 | */ 31 | queryInterface.removeColumn( 32 | 'Local', 33 | 'views' 34 | ); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /routes/revision.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let RevisionController = require('../controllers/RevisionController')(models.Revision) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/', RevisionController.getAll.bind(RevisionController)) 12 | router.get('/:_id', RevisionController.getById.bind(RevisionController)) 13 | router.post('/', AuthController.middlewareLogging, RevisionController.create.bind(RevisionController)) 14 | router.put('/:_id', AuthController.middlewareLogging, RevisionController.update.bind(RevisionController)) 15 | router.delete('/:_id', AuthController.middlewareLogging, RevisionController.remove.bind(RevisionController)) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6.1' 4 | env: 5 | - CXX=g++-4.8 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | packages: 11 | - g++-4.8 12 | deploy: 13 | app: bdb-api 14 | provider: heroku 15 | api_key: 16 | secure: Mwz/qY40ppUmdIxsOBaxLbBfnGB+eCBF0g+4BLUk5zywgPL/CHHeIPIDVYj8BpF+4IBGzS06ianjxFNC97XpbURqNwuLnfOnhDJttqgTgpRMpd0GLFPOlfUoYuwnOc0g6nFhfx6F46TURCJu3+GNB1e6OSH0FWq3N+DkRx5VGaKBm2MjW4wue3e99KPyN92NLdy87QQ63acE3l1vinkpRykSBvBrELkqo/1ZjQI8kVaLPevb4zvm0mWYg3fqBtwwKuajfE79FE+X4ugJPBfcpzT2oMUTidzVPnYZV08raxBJFzjvowRTaoEhlemSZ9eaoaa21xZaKWV7QIGWW6x+Bu1zDcHKvI5MyqJ+DliTIHTy5RtKw1FOKzIKXUI+F0eXlhUPjNtB0YIX05282foF5wzatFar9OB5lFJaoHWUJXo8ePulfapd3mENwq/MEH6QdW7uQg3Nsd4fXtpDwTRrLi3eqyBAhEvRNuN7L5L/TxOMnJCTWYEBcl85PRFLUry4xYHWMfMVIXA8rw84tjSvfTXMFXIfIwlgd5vWqx5Sthi0pAU2o3y3bG3xnF5yH5lZ/FptzkglkRKlPGeVV/fOOcJEzFLV5YixhVAZLClK4EavFWH1n/mopb6CrXacFlZdcidP9qrbTo8K8qO4DFGTGfUjpJbTqiNiSfAngG5PVBQ= 17 | -------------------------------------------------------------------------------- /models/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Log = sequelize.define('Log', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | user: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | role: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | endpoint: { 18 | type: DataTypes.STRING, 19 | allowNull: false 20 | }, 21 | body: { 22 | type: DataTypes.JSON 23 | }, 24 | method: { 25 | type: DataTypes.STRING 26 | }, 27 | ip_origin: { 28 | type: DataTypes.STRING 29 | } 30 | }, { 31 | timestamps: true, 32 | freezeTableName: true 33 | }); 34 | return Log; 35 | }; 36 | -------------------------------------------------------------------------------- /routes/datasource.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let DataSourceController = require('../controllers/DataSourceController')(models.DataSource) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/', DataSourceController.getAll.bind(DataSourceController)) 12 | router.get('/:_id', DataSourceController.getById.bind(DataSourceController)) 13 | router.post('/', AuthController.middlewareLogging, DataSourceController.create.bind(DataSourceController)) 14 | router.put('/:_id', AuthController.middlewareLogging, DataSourceController.update.bind(DataSourceController)) 15 | router.delete('/:_id', AuthController.middlewareLogging, DataSourceController.remove.bind(DataSourceController)) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /views/review/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /review 5 |
    6 |
    7 |

    Response Body

    8 |
     9 | [
    10 |   {
    11 |   "id": 53,
    12 |   "rating": 3,
    13 |   "hour": "16:35:28",
    14 |   "date": "2016-11-26T00:00:00.000Z",
    15 |   "createdAt": "2016-11-26T16:35:28.768Z",
    16 |   "updatedAt": "2016-11-26T16:35:28.768Z",
    17 |   "idLocal": 1123,
    18 |     "Tags": [
    19 |       {
    20 |       "id": 1,
    21 |       "name": "Iluminado",
    22 |       "createdAt": "2016-11-08T14:56:40.207Z",
    23 |       "updatedAt": "2016-11-08T14:56:40.207Z",
    24 |         "Review_Tags": {
    25 |           "createdAt": "2016-11-26T16:35:29.091Z",
    26 |           "updatedAt": "2016-11-26T16:35:29.091Z",
    27 |           "review_id": 53,
    28 |           "tag_id": 1
    29 |           }
    30 |       }
    31 |     ]
    32 |   }
    33 |  ]
    34 | 
    35 |
    36 |
  • 37 | -------------------------------------------------------------------------------- /routes/tag.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let TagController = require('../controllers/TagController')(models.Tag) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | // router.use(AuthController.middlewareAuth) 9 | // router.use(acl.authorize) 10 | 11 | router.get('/', TagController.getAll.bind(TagController)) 12 | router.get('/:_id', AuthController.middlewareAuth, acl.authorize, TagController.getById.bind(TagController)) 13 | router.post('/', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, TagController.create.bind(TagController)) 14 | router.put('/:_id', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, TagController.update.bind(TagController)) 15 | router.delete('/:_id', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, TagController.remove.bind(TagController)) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /routes/local.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let LocalController = require('../controllers/LocalController')(models.Local) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.get('/', AuthController.middlewareAuth, acl.authorize, LocalController.getAll.bind(LocalController)) 9 | router.get('/light', LocalController.getAllLight.bind(LocalController)) 10 | router.get('/:_id', LocalController.getById.bind(LocalController)) 11 | router.post('/', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, LocalController.create.bind(LocalController)) 12 | router.put('/:_id', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, LocalController.update.bind(LocalController)) 13 | router.delete('/:_id', AuthController.middlewareAuth, acl.authorize, AuthController.middlewareLogging, LocalController.remove.bind(LocalController)) 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /views/local/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /local 5 |
    6 |
    7 |

    Response Body

    8 |
     9 |     [
    10 |       {
    11 |         "address": "Largo Prof. Francisco de Paula Brochado Rocha - Farroupilha, Porto Alegre - RS, 90040-080, Brasil"
    12 |         "average": "4.0000000000000000"
    13 |         "checkins": "0"
    14 |         "createdAt": "2016-11-19T14:23:55.454Z"
    15 |         "description": "Perto do parquinho das crianças e do café."
    16 |         "id": 921
    17 |         "isPublic": true
    18 |         "lat": "-30.0358392"
    19 |         "lng": "-51.2178544"
    20 |         "photo": "https://s3.amazonaws.com/bikedeboa/images/921.jpeg"
    21 |         "reviews": "1"
    22 |         "structureType": "deroda"
    23 |         "text": "Redenção"
    24 |         "updatedAt": "2016-12-17T19:43:23.982Z"
    25 |       }
    26 |     ]
    27 |     
    28 |
    29 |
  • 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![alt tag](https://www.bikedeboa.com.br/favicons/icon-192x192.png) 3 | # Bike de boa 4 | 5 | API Webapp for colaboratively mapping your cities bike racks. 6 | 7 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 8 | [![Build Status](https://travis-ci.org/dennerevaldt/bikedeboa-api.svg?branch=master)](https://travis-ci.org/dennerevaldt/bikedeboa-api) 9 | 10 | ## API 11 | 12 | * [Documentation](https://bdb-api.herokuapp.com/v1/doc) 13 | 14 | This API data is licensed under [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/). It basically means you can use its data as you want as long as you give us proper credit. :) 15 | 16 | [![License: CC BY 4.0](https://licensebuttons.net/l/by/4.0/80x15.png)](http://creativecommons.org/licenses/by/4.0/) 17 | 18 | ## Contact 19 | 20 | [Email](bikedeboa@gmail.com)・[Facebook](https://www.facebook.com/bikedeboaapp)・[Instagram](https://www.instagram.com/bikedeboa/)・[Medium](https://medium.com/bike-de-boa) 21 | -------------------------------------------------------------------------------- /controllers/LogController.js: -------------------------------------------------------------------------------- 1 | function LogController (LogModel) { 2 | this.model = LogModel 3 | } 4 | 5 | LogController.prototype.getAll = function (request, response, next) { 6 | const _page = request.params._page; 7 | if (_page) { 8 | let limit = 100; // number of records per page 9 | let offset = 0; 10 | 11 | this.model.findAndCountAll() 12 | .then((data) => { 13 | let page = _page; // page number 14 | let pages = Math.ceil(data.count / limit); 15 | offset = limit * (page - 1); 16 | this.model.findAll({ 17 | limit: limit, 18 | offset: offset, 19 | $sort: { id: 1 } 20 | }) 21 | .then((logs) => { 22 | response.json({'result': logs, 'count': data.count, 'pages': pages}); 23 | }); 24 | }) 25 | .catch(function (error) { 26 | next(error) 27 | }); 28 | } else { 29 | this.model.findAll() 30 | .then((logs) => { 31 | response.json(logs); 32 | }); 33 | } 34 | } 35 | 36 | module.exports = function (LogModel) { 37 | return new LogController(LogModel) 38 | } 39 | -------------------------------------------------------------------------------- /views/log/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /log/:_page 5 |
    6 |
    7 |

    Parameters

    8 |

    _page | String | Log's _page

    9 | 10 |

    Response Body

    11 |
    12 | {
    13 |   "result": [
    14 |     {
    15 |       "id": 1,
    16 |       "user": "pedro",
    17 |       "role": "admin",
    18 |       "endpoint": "http://url.com/local",
    19 |       "body": {},
    20 |       "method": "GET",
    21 |       "createdAt": "2016-12-23T18:30:44.618Z",
    22 |       "updatedAt": "2016-12-23T18:30:44.618Z"
    23 |     },
    24 |     {
    25 |       "id": 2,
    26 |       "user": "pedro",
    27 |       "role": "colaborator",
    28 |       "endpoint": "http://url.com/local",
    29 |       "body": {},
    30 |       "method": "GET",
    31 |       "createdAt": "2016-12-23T18:33:15.053Z",
    32 |       "updatedAt": "2016-12-23T18:33:15.053Z"
    33 |     }
    34 |   ],
    35 |   "count": 2,
    36 |   "pages": 1
    37 | }
    38 | 
    39 |
    40 |
  • 41 | -------------------------------------------------------------------------------- /migrations/20161127140133-unnamed-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | return queryInterface.addColumn( 13 | 'Local', 14 | 'description', 15 | Sequelize.STRING 16 | ).then( function() { 17 | return queryInterface.addColumn( 18 | 'Local', 19 | 'address', 20 | Sequelize.STRING 21 | ); 22 | }); 23 | 24 | }, 25 | 26 | down: function (queryInterface, Sequelize) { 27 | /* 28 | Add reverting commands here. 29 | Return a promise to correctly handle asynchronicity. 30 | 31 | Example: 32 | return queryInterface.dropTable('users'); 33 | */ 34 | return queryInterface.removeColumn('Local', 'address').then( function() { 35 | return queryInterface.removeColumn('Local', 'description'); 36 | }) 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /migrations/20170811230516-CreateColumnsEmailAndFacebookID.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | return queryInterface.addColumn( 13 | 'User', 14 | 'email', 15 | Sequelize.STRING 16 | ).then( function() { 17 | return queryInterface.addColumn( 18 | 'User', 19 | 'facebook_id', 20 | Sequelize.STRING 21 | ); 22 | }); 23 | }, 24 | 25 | down: function (queryInterface, Sequelize) { 26 | /* 27 | Add reverting commands here. 28 | Return a promise to correctly handle asynchronicity. 29 | 30 | Example: 31 | return queryInterface.dropTable('users'); 32 | */ 33 | return queryInterface.removeColumn('User', 'facebook_id').then( function() { 34 | return queryInterface.removeColumn('User', 'email'); 35 | }) 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /views/local/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /local/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Local's _id

    9 | 10 |

    Response Body

    11 |
    12 | {
    13 |   "id": 1123,
    14 |   "lat": "-30.037524579889894",
    15 |   "lng": "-51.2110917075396",
    16 |   "structureType": "deroda",
    17 |   "isPublic": true,
    18 |   "text": "Prato Verde",
    19 |   "photo": "https://s3.amazonaws.com/bikedeboa/images/1123.jpeg",
    20 |   "description": "Próximo a garagem",
    21 |   "address": "Rua Santa Terezinha, 50 - Farroupilha, Porto Alegre - RS, 90040-180, Brasil",
    22 |   "reviews": "1",
    23 |   "average": "5.000000",
    24 |   "tags": [
    25 |     {
    26 |       "name": "Fácil acesso",
    27 |       "count": "1"
    28 |     },
    29 |     {
    30 |       "name": "Iluminado",
    31 |       "count": "1"
    32 |     },
    33 |     {
    34 |       "name": "Movimentado",
    35 |       "count": "1"
    36 |     }
    37 |   ]
    38 | }
    39 | 
    40 |
    41 |
  • 42 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | concat = require('gulp-concat'), 3 | minifycss = require('gulp-minify-css'), 4 | uglify = require('gulp-uglify'); 5 | 6 | var css = { 7 | source: 'src/stylesheets', 8 | target: 'public/stylesheets' 9 | }; 10 | var js = { 11 | source: 'src/javascripts', 12 | target: 'public/javascripts' 13 | }; 14 | 15 | gulp.task('css', function () { 16 | gulp.src([ 17 | css.source + '/normaset.css', 18 | css.source + '/*.css' 19 | ]) 20 | .pipe(concat('all.min.css')) 21 | .pipe(minifycss({keepSpecialComments: false})) 22 | .pipe(gulp.dest(css.target)); 23 | }); 24 | 25 | gulp.task('js', function() { 26 | gulp.src([ 27 | js.source + '/vendor/*.js', 28 | js.source + '/*.js' 29 | ]) 30 | .pipe(concat('all.min.js')) 31 | .pipe(uglify({mangle: true}).on('error', console.error)) 32 | .pipe(gulp.dest(js.target)); 33 | }); 34 | 35 | gulp.task('watch', function() { 36 | livereload.listen(); 37 | gulp.watch(css.source + '/*.css', ['css']); 38 | gulp.watch(js.source + '/*.js', ['js']); 39 | }); 40 | 41 | gulp.task('default', ['css', 'js']); 42 | -------------------------------------------------------------------------------- /views/review/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /review/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Review's _id

    9 | 10 |

    Response Body

    11 |
    12 | [
    13 |     {
    14 |     "id": 53,
    15 |     "rating": 3,
    16 |     "hour": "16:35:28",
    17 |     "date": "2016-11-26T00:00:00.000Z",
    18 |     "createdAt": "2016-11-26T16:35:28.768Z",
    19 |     "updatedAt": "2016-11-26T16:35:28.768Z",
    20 |     "idLocal": 1123,
    21 |       "Tags": [
    22 |         {
    23 |         "id": 1,
    24 |         "name": "Iluminado",
    25 |         "createdAt": "2016-11-08T14:56:40.207Z",
    26 |         "updatedAt": "2016-11-08T14:56:40.207Z",
    27 |           "Review_Tags": {
    28 |             "createdAt": "2016-11-26T16:35:29.091Z",
    29 |             "updatedAt": "2016-11-26T16:35:29.091Z",
    30 |             "review_id": 53,
    31 |             "tag_id": 1
    32 |             }
    33 |         }
    34 |       ]
    35 |     }
    36 |  ]
    37 | 
    38 |
    39 |
  • 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Denner Evaldt Machado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /views/revision/get.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /revision 5 |
    6 |
    7 |

    Response Body

    8 |
     9 |     [
    10 |       {
    11 |         "id": 2,
    12 |         "comments": "Teste",
    13 |         "createdAt": "2016-12-29T21:51:00.540Z",
    14 |         "updatedAt": "2016-12-29T21:51:00.540Z",
    15 |         "local_id": 921,
    16 |         "Local": {
    17 |           "id": 921,
    18 |           "lat": "-30.0358392",
    19 |           "lng": "-51.2178544",
    20 |           "structureType": "deroda",
    21 |           "isPublic": true,
    22 |           "text": "Redenção",
    23 |           "photo": "url",
    24 |           "description": "Perto do parquinho das crianças e do café.",
    25 |           "address": "Largo Prof. Francisco de Paula Brochado Rocha - Farroupilha, Porto Alegre - RS, 90040-080, Brasil",
    26 |           "createdAt": "2016-11-19T14:23:55.454Z",
    27 |           "updatedAt": "2016-12-17T19:43:23.982Z"
    28 |         }
    29 |       }
    30 |     ]
    31 |     
    32 |
    33 |
  • 34 | -------------------------------------------------------------------------------- /views/revision/get_id.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | GET 4 | /revision/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Revision's _id

    9 | 10 |

    Response Body

    11 |
    12 |     {
    13 |       "id": 2,
    14 |       "comments": "Teste",
    15 |       "createdAt": "2016-12-29T21:51:00.540Z",
    16 |       "updatedAt": "2016-12-29T21:51:00.540Z",
    17 |       "local_id": 921,
    18 |       "Local": {
    19 |         "id": 921,
    20 |         "lat": "-30.0358392",
    21 |         "lng": "-51.2178544",
    22 |         "structureType": "deroda",
    23 |         "isPublic": true,
    24 |         "text": "Redenção",
    25 |         "photo": "url",
    26 |         "description": "Perto do parquinho das crianças e do café.",
    27 |         "address": "Largo Prof. Francisco de Paula Brochado Rocha - Farroupilha, Porto Alegre - RS, 90040-080, Brasil",
    28 |         "createdAt": "2016-11-19T14:23:55.454Z",
    29 |         "updatedAt": "2016-12-17T19:43:23.982Z"
    30 |       }
    31 |     }
    32 |     
    33 |
    34 |
  • 35 | -------------------------------------------------------------------------------- /models/review.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Review = sequelize.define('Review', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | description: { 10 | type: DataTypes.STRING 11 | }, 12 | rating: { 13 | type: DataTypes.INTEGER 14 | }, 15 | hour: { 16 | type: DataTypes.TIME, 17 | allowNull: false 18 | }, 19 | date: { 20 | type: DataTypes.DATE, 21 | allowNull: false 22 | } 23 | }, { 24 | timestamps: true, 25 | freezeTableName: true, 26 | classMethods: { 27 | associate: function(models) { 28 | // associations can be defined here 29 | Review.belongsToMany(models.Tag, {through: 'Review_Tags', foreignKey: 'review_id', otherKey: 'tag_id', hooks: true}); 30 | Review.belongsTo(models.User, {foreignKey: 'user_id', hooks: true}); 31 | Review.belongsTo(models.Local, {foreignKey: 'local_id', hooks: true}); 32 | } 33 | } 34 | }); 35 | return Review; 36 | }; -------------------------------------------------------------------------------- /views/local/post.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | POST 4 | /local 5 |
    6 |
    7 |

    Request Body

    8 |
     9 |     {
    10 |       "lat": "-30.037524579889894",
    11 |       "lng": "-51.2110917075396",
    12 |       "structureType": "deroda",
    13 |       "isPublic": true,
    14 |       "text": "Prato Verde",
    15 |       "photo": (blob64),
    16 |       "authorIP": "199.199.99.9",
    17 |       "description": "Próximo a garagem.",
    18 |       "address": "Rua Santa Terezinha, 50 - Farroupilha, Porto Alegre - RS, 90040-180, Brasil"
    19 |     }
    20 |     
    21 | 22 |

    Response Body

    23 |
    24 |      {
    25 |         "id": 1123,
    26 |         "lat": "-30.037524579889894",
    27 |         "lng": "-51.2110917075396",
    28 |         "structureType": "deroda",
    29 |         "isPublic": true,
    30 |         "text": "Prato Verde",
    31 |         "photo": "https://s3.amazonaws.com/bikedeboa/images/1123.jpeg",
    32 |         "authorIP": "199.199.99.9",
    33 |         "description": "Próximo a garagem.",
    34 |         "address": "Rua Santa Terezinha, 50 - Farroupilha, Porto Alegre - RS, 90040-180, Brasil",
    35 |         "reviews": "0",
    36 |         "checkins": "0",
    37 |         "tags": []
    38 |       }
    39 |     
    40 |
    41 |
  • 42 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var basename = path.basename(module.filename); 7 | var env = process.env.NODE_ENV || 'development'; 8 | var config = require(__dirname + '/../config/config.json')[env]; 9 | var db = {}; 10 | 11 | if (config.use_env_variable) { 12 | var sequelize = new Sequelize(process.env[config.use_env_variable], { 13 | dialectOptions: { 14 | ssl: true 15 | } 16 | }); 17 | } else { 18 | if (env === 'production') { 19 | var sequelize = new Sequelize(process.env.DATABASE_URL, { 20 | dialectOptions: { 21 | ssl: true 22 | } 23 | }); 24 | } else { 25 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 26 | } 27 | } 28 | 29 | fs 30 | .readdirSync(__dirname) 31 | .filter(function(file) { 32 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 33 | }) 34 | .forEach(function(file) { 35 | var model = sequelize['import'](path.join(__dirname, file)); 36 | db[model.name] = model; 37 | }); 38 | 39 | Object.keys(db).forEach(function(modelName) { 40 | if (db[modelName].associate) { 41 | db[modelName].associate(db); 42 | } 43 | }); 44 | 45 | db.sequelize = sequelize; 46 | db.Sequelize = Sequelize; 47 | 48 | module.exports = db; 49 | -------------------------------------------------------------------------------- /migrations/20170812005551-ChangeNotNullColumnsForUsers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | 13 | return queryInterface.changeColumn( 14 | 'User', 15 | 'username', 16 | { 17 | type: Sequelize.STRING, 18 | allowNull: true 19 | } 20 | ).then( function() { 21 | return queryInterface.changeColumn( 22 | 'User', 23 | 'password', 24 | { 25 | type: Sequelize.STRING, 26 | allowNull: true 27 | } 28 | ); 29 | }); 30 | }, 31 | 32 | down: function (queryInterface, Sequelize) { 33 | /* 34 | Add reverting commands here. 35 | Return a promise to correctly handle asynchronicity. 36 | 37 | Example: 38 | return queryInterface.dropTable('users'); 39 | */ 40 | 41 | return queryInterface.changeColumn( 42 | 'User', 43 | 'username', 44 | { 45 | type: Sequelize.STRING, 46 | allowNull: false 47 | } 48 | ).then( function() { 49 | return queryInterface.changeColumn( 50 | 'User', 51 | 'password', 52 | { 53 | type: Sequelize.STRING, 54 | allowNull: false 55 | } 56 | ); 57 | }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /controllers/CheckinController.js: -------------------------------------------------------------------------------- 1 | let moment = require('moment') 2 | 3 | // PRIVATE FN 4 | 5 | let handleNotFound = function (data) { 6 | if (!data) { 7 | let err = new Error('Not Found') 8 | err.status = 404 9 | throw err 10 | } 11 | return data 12 | } 13 | 14 | // PRIVATE FN 15 | 16 | function CheckinController (CheckinModel) { 17 | this.model = CheckinModel 18 | } 19 | 20 | CheckinController.prototype.getAll = function (request, response, next) { 21 | var _query = {} 22 | 23 | this.model.findAll(_query) 24 | .then(function (data) { 25 | response.json(data) 26 | }) 27 | .catch(next) 28 | } 29 | 30 | CheckinController.prototype.getById = function (request, response, next) { 31 | var _query = { 32 | where: {id: request.params._id} 33 | } 34 | 35 | this.model.find(_query) 36 | .then(handleNotFound) 37 | .then(function (data) { 38 | response.json(data) 39 | }) 40 | .catch(next) 41 | } 42 | 43 | CheckinController.prototype.create = function (request, response, next) { 44 | let _body = request.body 45 | let _currentDate = moment().format('YYYY-MM-DD') 46 | let _currentHour = moment().format('YYYY-MM-DD HH:mm:ss') 47 | let _checkIn = { 48 | local_id: _body.idLocal, 49 | date: _currentDate, 50 | hour: _currentHour 51 | } 52 | 53 | this.model.create(_checkIn) 54 | .then(function (data) { 55 | response.json(data) 56 | }) 57 | .catch(next) 58 | } 59 | 60 | module.exports = function (CheckinModel) { 61 | return new CheckinController(CheckinModel) 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@denner/bikedeboa-api", 3 | "version": "1.0.0", 4 | "description": "API Webapp for colaboratively mapping your cities bike racks", 5 | "main": "./bin/www", 6 | "private": false, 7 | "scripts": { 8 | "start": "node ./bin/www", 9 | "nodemon": "nodemon --inspect ./bin/www", 10 | "test": "" 11 | }, 12 | "engines": { 13 | "node": "8.6.0" 14 | }, 15 | "author": "Denner Evaldt Machado ", 16 | "license": "MIT", 17 | "dependencies": { 18 | "aws-sdk": "^2.1644.0", 19 | "bcrypt-nodejs": "0.0.3", 20 | "body-parser": "^1.20.2", 21 | "compression": "^1.7.4", 22 | "config": "^1.31.0", 23 | "cors": "^2.8.5", 24 | "debug": "^2.2.0", 25 | "dotenv": "^16.4.5", 26 | "express": "^4.19.2", 27 | "express-acl": "^1.0.0", 28 | "helmet": "^3.23.3", 29 | "install": "^0.8.4", 30 | "jwt-simple": "^0.5.6", 31 | "method-override": "^2.3.6", 32 | "moment": "^2.30.1", 33 | "node-uuid": "^1.4.7", 34 | "npm": "^6.14.18", 35 | "pg": "^6.1.0", 36 | "pg-hstore": "^2.3.4", 37 | "request": "^2.88.2", 38 | "sequelize": "^3.35.1", 39 | "sequelize-cli": "^2.8.0", 40 | "sharp": "^0.17.1", 41 | "swig": "^1.4.2" 42 | }, 43 | "devDependencies": { 44 | "chai": "^3.5.0", 45 | "gulp": "^3.9.1", 46 | "gulp-concat": "^2.6.1", 47 | "gulp-minify-css": "^1.2.4", 48 | "gulp-uglify": "^2.1.2", 49 | "mocha": "^3.2.0", 50 | "nodemon": "^1.19.4", 51 | "standard": "^8.6.0", 52 | "supertest": "^2.0.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let router = express.Router() 3 | let acl = require('express-acl') 4 | let models = require('../models') 5 | let UserController = require('../controllers/UserController')(models.User) 6 | let AuthController = require('../controllers/AuthController')(models.User) 7 | 8 | router.use(AuthController.middlewareAuth) 9 | router.use(acl.authorize) 10 | 11 | router.get('/', UserController.getAll.bind(UserController)) 12 | router.get('/reviews', UserController.getCurrentUserReviews.bind(UserController)) 13 | router.get('/locals', UserController.getCurrentUserLocals.bind(UserController)) 14 | router.get('/current', UserController.getCurrentUser.bind(UserController)) 15 | router.get('/:_id', UserController.getById.bind(UserController)) 16 | router.get('/:_id/locals', UserController.getUserLocals.bind(UserController)) 17 | router.get('/:_id/reviews', UserController.getUserReviews.bind(UserController)) 18 | router.post('/', AuthController.middlewareLogging, UserController.create.bind(UserController)) 19 | router.put('/:_id', AuthController.middlewareLogging, UserController.update.bind(UserController)) 20 | router.post('/import-reviews', UserController.importReviewsToCurrentUser.bind(UserController)) 21 | router.post('/import-locals', UserController.importLocalsToCurrentUser.bind(UserController)) 22 | router.delete('/:_id', AuthController.middlewareLogging, UserController.remove.bind(UserController)) 23 | router.delete('/', AuthController.middlewareLogging, UserController.removeAll.bind(UserController)) 24 | 25 | module.exports = router 26 | -------------------------------------------------------------------------------- /views/local/put.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | PUT 4 | /local/:_id 5 |
    6 |
    7 |

    Parameters

    8 |

    _id | String | Local's _id

    9 | 10 |

    Request Body

    11 |
    12 |     {
    13 |       "id": "1123",
    14 |       "lat": "-30.037524579889894",
    15 |       "lng": "-51.2110917075396",
    16 |       "structureType": "deroda",
    17 |       "isPublic": true,
    18 |       "text": "Prato Verde",
    19 |       "photo": (blob64),
    20 |       "description": "Próximo a garagem",
    21 |       "address": "Rua Santa Terezinha, 50 - Farroupilha, Porto Alegre - RS, 90040-180, Brasil",
    22 |       "photoUrl": "(url photo update)"
    23 |     }
    24 |     
    25 | 26 |

    Response Body

    27 |
    28 |     {
    29 |       "lat": "-30.037524579889894",
    30 |       "lng": "-51.2110917075396",
    31 |       "structureType": "deroda",
    32 |       "isPublic": true,
    33 |       "text": "Prato Verde",
    34 |       "photo": "https://s3.amazonaws.com/bikedeboa/images/1123.jpeg",
    35 |       "description": "Próximo a garagem.",
    36 |       "address": "Rua Santa Terezinha, 50 - Farroupilha, Porto Alegre - RS, 90040-180, Brasil",
    37 |       "tags": [
    38 |         {
    39 |           "name": "Espaçoso",
    40 |           "count": "1"
    41 |         },
    42 |         {
    43 |           "name": "Fácil acesso",
    44 |           "count": "1"
    45 |         },
    46 |         {
    47 |           "name": "Movimentado",
    48 |           "count": "1"
    49 |         }
    50 |       ]
    51 |     }
    52 |     
    53 |
    54 |
  • 55 | -------------------------------------------------------------------------------- /migrations/20171213000916-AddCityStateToLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 11 | */ 12 | 13 | queryInterface.addColumn( 14 | 'Local', 15 | 'city', 16 | { 17 | type: Sequelize.STRING, 18 | allowNull: true 19 | } 20 | ).then( function() { 21 | return queryInterface.addColumn( 22 | 'Local', 23 | 'state', 24 | { 25 | type: Sequelize.STRING, 26 | allowNull: true 27 | } 28 | ); 29 | }).then( function() { 30 | return queryInterface.addColumn( 31 | 'Local', 32 | 'country', 33 | { 34 | type: Sequelize.STRING, 35 | allowNull: true 36 | } 37 | ); 38 | }); 39 | }, 40 | 41 | down: function (queryInterface, Sequelize) { 42 | /* 43 | Add reverting commands here. 44 | Return a promise to correctly handle asynchronicity. 45 | 46 | Example: 47 | return queryInterface.dropTable('users'); 48 | */ 49 | // return queryInterface.removeColumn('User', 'facebook_id').then( function() { 50 | // return queryInterface.removeColumn('User', 'email'); 51 | // }) 52 | queryInterface.removeColumn('Local','city') 53 | .then( function() { 54 | queryInterface.removeColumn('Local','state') 55 | }) 56 | .then( function() { 57 | queryInterface.removeColumn('Local','country') 58 | }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /config/acl.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "group": "client", 4 | "permissions": [ 5 | {"resource": "user", "methods": ["GET","POST"], "action": "allow"}, 6 | {"resource": "review", "methods": ["GET"], "action": "allow"}, 7 | {"resource": "local", "methods": ["GET"], "action": "allow"}, 8 | {"resource": "tag", "methods": ["GET"], "action": "allow"}, 9 | {"resource": "log", "methods": "*", "action": "deny"}, 10 | {"resource": "revision", "methods": ["POST"], "action": "allow"}, 11 | {"resource": "datasource", "methods": ["GET"], "action": "allow"}, 12 | {"resource": "stats", "methods": ["GET"], "action": "allow"} 13 | ] 14 | }, 15 | { 16 | "group": "user", 17 | "permissions": [ 18 | {"resource": "user", "methods": ["GET","POST"], "action": "allow"}, 19 | {"resource": "review", "methods": "*", "action": "allow"}, 20 | {"resource": "local", "methods": "*", "action": "allow"}, 21 | {"resource": "tag", "methods": ["GET"], "action": "allow"}, 22 | {"resource": "log", "methods": "*", "action": "deny"}, 23 | {"resource": "revision", "methods": ["POST"], "action": "allow"}, 24 | {"resource": "datasource", "methods": ["GET"], "action": "allow"}, 25 | {"resource": "stats", "methods": ["GET"], "action": "allow"} 26 | ] 27 | }, 28 | { 29 | "group": "colaborator", 30 | "permissions": [ 31 | {"resource": "user", "methods": ["GET","POST"], "action": "allow"}, 32 | {"resource": "review", "methods": "*", "action": "allow"}, 33 | {"resource": "local", "methods": "*", "action": "allow"}, 34 | {"resource": "tag", "methods": ["GET"], "action": "allow"}, 35 | {"resource": "log", "methods": "*", "action": "deny"}, 36 | {"resource": "revision", "methods": "*", "action": "allow"}, 37 | {"resource": "datasource", "methods": ["GET"], "action": "allow"}, 38 | {"resource": "stats", "methods": "*", "action": "allow"} 39 | ] 40 | }, 41 | { 42 | "group": "admin", 43 | "permissions": [ 44 | {"resource": "*", "methods": "*", "action": "allow"} 45 | ] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let methodOverride = require('method-override') 3 | let bodyParser = require('body-parser') 4 | let cors = require('cors') 5 | let app = express() 6 | let path = require('path') 7 | let swig = require('swig') 8 | let acl = require('express-acl') 9 | let helmet = require('helmet') 10 | let compression = require('compression') 11 | 12 | // server config 13 | app.use(methodOverride('X-HTTP-Method')) 14 | app.use(methodOverride('X-HTTP-Method-Override')) 15 | app.use(methodOverride('X-Method-Override')) 16 | app.use(methodOverride('_method')) 17 | app.use(bodyParser.json({limit: '50mb'})) 18 | app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })) 19 | app.use(helmet()) 20 | app.use(cors({ 21 | // origin: ['https://www.bikedeboa.com.br','https://bikedeboa-dev.herokuapp.com','https://bikedeboa-dev2.herokuapp.com'], 22 | origin: [true], 23 | methods: ['GET', 'POST', 'PUT', 'DELETE'], 24 | allowedHeaders: ['Content-Type', 'x-access-token', 'ip_origin'] 25 | })) 26 | app.use(compression()) 27 | 28 | // static 29 | app.use(express.static(path.join(__dirname, 'public'))) 30 | 31 | // setup swig 32 | app.set('view engine', 'html') 33 | app.set('views', path.join(__dirname, 'views')) 34 | app.engine('html', swig.renderFile) 35 | 36 | // setup acl 37 | acl.config({ 38 | baseUrl: '/', 39 | filename: 'acl.json', 40 | path: 'config' 41 | }) 42 | 43 | // skip favicon 44 | app.use(function (request, response, next) { 45 | if (request.url === '/favicon.ico') { 46 | response.writeHead(200, {'Content-Type': 'image/x-icon'}) 47 | response.end('') 48 | } else { 49 | next() 50 | } 51 | }) 52 | 53 | // router 54 | app.use('/', require('./routes')) 55 | 56 | // error handling 57 | app.use(function (request, response, next) { 58 | var err = new Error('Not Found') 59 | err.status = 404 60 | next(err) 61 | }) 62 | 63 | app.use(function (err, request, response, next) { 64 | response.status(err.status || 500).json({ error: err.errors || err.message }) 65 | }) 66 | 67 | // server listener 68 | module.exports = app 69 | 70 | // start api: export DEBUG=api:* && export JWT_TKN_SECRET=testBdb && export NODE_ENV=development && export SUPPRESS_NO_CONFIG_WARNING=false && npm run nodemon 71 | // see fixes pattern code: standard 'app.js' 'routes/*.js' 'controllers/*.js' 72 | -------------------------------------------------------------------------------- /controllers/TagController.js: -------------------------------------------------------------------------------- 1 | // PRIVATE FN 2 | 3 | var handleNotFound = function (data) { 4 | if (!data) { 5 | let err = new Error('Not Found') 6 | err.status = 404 7 | throw err 8 | } 9 | return data 10 | } 11 | 12 | // PRIVATE FN 13 | 14 | function TagController (TagModel) { 15 | this.model = TagModel 16 | } 17 | 18 | TagController.prototype.getAll = function (request, response, next) { 19 | let _query = {} 20 | 21 | this.model.findAll(_query) 22 | .then(function (data) { 23 | response.json(data) 24 | }) 25 | .catch(next) 26 | } 27 | 28 | TagController.prototype.getById = function (request, response, next) { 29 | let _query = { 30 | where: {id: request.params._id} 31 | } 32 | 33 | this.model.find(_query) 34 | .then(handleNotFound) 35 | .then(function (data) { 36 | response.json(data) 37 | }) 38 | .catch(next) 39 | } 40 | 41 | TagController.prototype.create = function (request, response, next) { 42 | let _body = request.body 43 | let _tag = { 44 | name: _body.name 45 | } 46 | 47 | this.model.create(_tag) 48 | .then(function (data) { 49 | response.json(data) 50 | }) 51 | .catch(next) 52 | } 53 | 54 | TagController.prototype.update = function (request, response, next) { 55 | let _id = request.params._id 56 | let _body = request.body 57 | let _tag = { 58 | name: _body.name 59 | } 60 | let _query = { 61 | where: {id: _id} 62 | } 63 | 64 | this.model.find(_query) 65 | .then(handleNotFound) 66 | .then(function (data) { 67 | data.update(_tag) 68 | .then(function (tag) { 69 | response.json(tag) 70 | return tag 71 | }) 72 | .catch(next) 73 | return data 74 | }) 75 | .catch(next) 76 | } 77 | 78 | TagController.prototype.remove = function (request, response, next) { 79 | let _id = request.params._id 80 | let _query = { 81 | where: {id: _id} 82 | } 83 | 84 | this.model.destroy(_query) 85 | .then(handleNotFound) 86 | .then(function (rowDeleted) { 87 | if (rowDeleted === 1) { 88 | response.json({ 89 | message: 'Deleted successfully' 90 | }) 91 | } 92 | }) 93 | .catch(next) 94 | } 95 | 96 | module.exports = function (TagModel) { 97 | return new TagController(TagModel) 98 | } 99 | -------------------------------------------------------------------------------- /models/local.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function(sequelize, DataTypes) { 3 | var Local = sequelize.define('Local', { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true 8 | }, 9 | lat: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | lng: { 14 | type: DataTypes.STRING, 15 | allowNull: false 16 | }, 17 | structureType: { 18 | type: DataTypes.STRING 19 | }, 20 | isPublic: { 21 | type: DataTypes.BOOLEAN 22 | }, 23 | isCovered: { 24 | type: DataTypes.BOOLEAN 25 | }, 26 | isPaid: { 27 | type: DataTypes.BOOLEAN 28 | }, 29 | text: { 30 | type: DataTypes.TEXT 31 | }, 32 | photo: { 33 | type: DataTypes.STRING 34 | }, 35 | description: { 36 | type: DataTypes.TEXT 37 | }, 38 | address: { 39 | type: DataTypes.TEXT 40 | }, 41 | authorIP: { 42 | type: DataTypes.STRING 43 | }, 44 | city: { 45 | type: DataTypes.STRING 46 | }, 47 | state: { 48 | type: DataTypes.STRING 49 | }, 50 | country: { 51 | type: DataTypes.STRING 52 | }, 53 | views: { 54 | type: DataTypes.INTEGER, 55 | }, 56 | slots: { 57 | type: DataTypes.INTEGER, 58 | } 59 | }, { 60 | timestamps: true, 61 | freezeTableName: true, 62 | classMethods: { 63 | associate: function(models) { 64 | // associations can be defined here 65 | Local.belongsToMany(models.Tag, {through: 'Local_Tags', foreignKey: 'local_id', otherKey: 'tag_id', hooks: true}); 66 | Local.hasMany(models.Review, {foreignKey: 'local_id', onDelete: 'cascade', hooks: true}); 67 | Local.hasOne(models.Checkin, {foreignKey: 'local_id', onDelete: 'cascade', hooks: true}); 68 | Local.hasMany(models.Revision, {foreignKey: 'local_id', onDelete: 'cascade', hooks: true}); 69 | Local.belongsTo(models.User, {foreignKey: 'user_id', hooks: true}); 70 | Local.belongsTo(models.DataSource, { foreignKey: 'datasource_id', hooks: false }); 71 | } 72 | }, 73 | instanceMethods: { 74 | } 75 | }); 76 | return Local; 77 | }; 78 | -------------------------------------------------------------------------------- /controllers/RevisionController.js: -------------------------------------------------------------------------------- 1 | let models = require('../models') 2 | 3 | // PRIVATE FN 4 | 5 | let handleNotFound = function (data) { 6 | if (!data) { 7 | let err = new Error('Not Found') 8 | err.status = 404 9 | throw err 10 | } 11 | return data 12 | } 13 | 14 | // PRIVATE FN 15 | 16 | function RevisionController (RevisionModel) { 17 | this.model = RevisionModel 18 | } 19 | 20 | RevisionController.prototype.getAll = function (request, response, next) { 21 | let _query = { 22 | include: [models.Local] 23 | } 24 | 25 | this.model.findAll(_query) 26 | .then(function (data) { 27 | response.json(data) 28 | }) 29 | .catch(next) 30 | } 31 | 32 | RevisionController.prototype.getById = function (request, response, next) { 33 | let _query = { 34 | where: {id: request.params._id}, 35 | include: [models.Local] 36 | } 37 | 38 | this.model.find(_query) 39 | .then(handleNotFound) 40 | .then(function (data) { 41 | response.json(data) 42 | }) 43 | .catch(next) 44 | } 45 | 46 | RevisionController.prototype.create = function (request, response, next) { 47 | let _body = request.body 48 | let _revision = { 49 | local_id: _body.local_id, 50 | comments: _body.comments 51 | } 52 | 53 | this.model.create(_revision) 54 | .then(function (data) { 55 | response.json(data) 56 | }) 57 | .catch(next) 58 | } 59 | 60 | RevisionController.prototype.update = function (request, response, next) { 61 | let _id = request.params._id 62 | let _body = request.body 63 | let _revision = { 64 | comments: _body.comments 65 | } 66 | let _query = { 67 | where: {id: _id} 68 | } 69 | 70 | this.model.find(_query) 71 | .then(handleNotFound) 72 | .then(function (data) { 73 | data.update(_revision) 74 | .then(function (tag) { 75 | response.json(tag) 76 | return tag 77 | }) 78 | .catch(next) 79 | return data 80 | }) 81 | .catch(next) 82 | } 83 | 84 | RevisionController.prototype.remove = function (request, response, next) { 85 | let _id = request.params._id 86 | let _query = { 87 | where: {id: _id} 88 | } 89 | 90 | this.model.destroy(_query) 91 | .then(handleNotFound) 92 | .then(function (rowDeleted) { 93 | if (rowDeleted === 1) { 94 | response.json({ 95 | message: 'Deleted successfully' 96 | }) 97 | } 98 | }) 99 | .catch(next) 100 | } 101 | 102 | module.exports = function (RevisionModel) { 103 | return new RevisionController(RevisionModel) 104 | } 105 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var bcrypt = require('bcrypt-nodejs'); 3 | 4 | module.exports = function(sequelize, DataTypes) { 5 | var User = sequelize.define('User', { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | fullname: { 12 | type: DataTypes.STRING, 13 | validate: { 14 | len: { 15 | args: [5, 50], 16 | msg: 'Seu nome completo deve ter no mínimo 5 e no máximo 50 caracteres.' 17 | } 18 | } 19 | }, 20 | username: { 21 | type: DataTypes.STRING, 22 | unique: { 23 | msg: 'Usuário já existe.' 24 | } 25 | }, 26 | password: { 27 | type: DataTypes.STRING, 28 | validate: { 29 | len: { 30 | args: [6, 100], 31 | msg: 'Sua senha deve ter no mínimo 6 e no máximo 8 caracteres.' 32 | } 33 | } 34 | }, 35 | role: { 36 | type: DataTypes.STRING 37 | }, 38 | email: { 39 | type: DataTypes.STRING, 40 | }, 41 | facebook_id: { 42 | type: DataTypes.STRING, 43 | unique: { 44 | msg: 'Facebook profile exists already.' 45 | } 46 | }, 47 | google_id: { 48 | type: DataTypes.STRING, 49 | unique: { 50 | msg: 'Google profile exists already.' 51 | } 52 | } 53 | }, 54 | { 55 | timestamps: true, 56 | freezeTableName: true, 57 | classMethods: { 58 | associate: function(models) { 59 | User.hasMany(models.Review, {foreignKey: 'user_id', hooks: true}); 60 | User.hasMany(models.Local, {foreignKey: 'user_id', hooks: true}); 61 | } 62 | }, 63 | hooks: { 64 | beforeValidate: function (user, options) { 65 | if (typeof user.username === 'string') { 66 | user.username = user.username.toLowerCase(); 67 | } 68 | }, 69 | beforeCreate: function(user, options) { 70 | if (user.password) { 71 | user.password = bcrypt.hashSync(user.password); 72 | } 73 | } 74 | }, 75 | instanceMethods : { 76 | validPassword: function(password, hash){ 77 | return bcrypt.compareSync(password, hash); 78 | } 79 | } 80 | }); 81 | return User; 82 | }; 83 | -------------------------------------------------------------------------------- /src/stylesheets/api.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 14px 'Open Sans', sans-serif; 3 | padding-top: 70px; 4 | color: #333; 5 | min-width: 300px; 6 | } 7 | a { text-decoration: none; } 8 | a:hover { text-decoration: underline; } 9 | p { margin-bottom: 20px; } 10 | h1, h2, h3 { font-weight: bold; margin-bottom: 15px; } 11 | h1 { font-size: 1.5em; } 12 | h2 { font-size: 1.2em; } 13 | h3 { font-size: 1.1em; } 14 | .fleft { float: left; } 15 | #header { 16 | background-color: #034482; 17 | background-image: linear-gradient(to bottom, #323232, #222); 18 | width: 100%; height: 50px; line-height: 50px; 19 | position: fixed; 20 | top: 0; left: 0; 21 | } 22 | #header-title { 23 | color: #fff; 24 | } 25 | .content { 26 | width: 1180px; 27 | margin: 0 auto; 28 | } 29 | .api-item-title { 30 | color: #999; 31 | cursor: pointer; 32 | border-bottom: 1px solid #ddd; 33 | padding-bottom: 20px; 34 | } 35 | .api-item-title:hover { color: #000; } 36 | .endpoints { 37 | width: 80%; 38 | margin: 20px auto; 39 | } 40 | .endpoint-item { 41 | margin-bottom: 15px; border-radius: 3px; 42 | } 43 | .noun { 44 | color: #fff; 45 | border-radius: 3px; 46 | text-align: center; 47 | width: 60px; 48 | height: 25px; line-height: 25px; 49 | display: inline-block; 50 | font-size: 0.7em; 51 | } 52 | .endpoint-heading { overflow: hidden; } 53 | .endpoint-name { font-weight: bold; margin-left: 10px; padding-top: 4px; color: #000; font-size: 0.9em; } 54 | .noun-get { background-color: #E7F0F7; border: 1px solid #c3d9ec } 55 | .noun-post { background-color: #E7F6EC; border: 1px solid #c3e8d1 } 56 | .noun-put { background-color: #f6e7f7; border: 1px solid #dcaaf7 } 57 | .noun-delete { background-color: #ffe1e1; border: 1px solid #ee9898 } 58 | .noun-get h3 { color: #0F6AB4; } 59 | .noun-post h3 { color: #10A54A; } 60 | .noun-put h3 { color: #8320b7; } 61 | .noun-delete h3 { color: #dc2f2f; } 62 | .noun-get .endpoint-heading { border-bottom: 1px solid #c3d9ec } 63 | .noun-post .endpoint-heading { border-bottom: 1px solid #c3e8d1 } 64 | .noun-put .endpoint-heading { border-bottom: 1px solid #dcaaf7 } 65 | .noun-delete .endpoint-heading { border-bottom: 1px solid #ee9898 } 66 | .noun-get .noun { background-color: #0F6AB4; } 67 | .noun-post .noun { background-color: #10A54A; } 68 | .noun-put .noun { background-color: #8320b7; } 69 | .noun-delete .noun { background-color: #dc2f2f; } 70 | .endpoint-content { padding: 15px; } 71 | pre { 72 | background: #F5F5F5; 73 | border: 1px solid #DCDCDC; border-radius: 3px; 74 | min-height: 15px; padding: 4px 15px; 75 | } 76 | @media only screen and (max-width: 1180px) { 77 | body { font-size: 12px; } 78 | .content, 79 | .endpoints { width: 95%; } 80 | pre { overflow: auto; font-size: 0.9em; } 81 | } 82 | @media only screen and (max-width: 680px) { 83 | pre { overflow: auto; font-size: 0.8em; } 84 | } 85 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /controllers/DataSourceController.js: -------------------------------------------------------------------------------- 1 | let models = require('../models') 2 | 3 | // PRIVATE FN 4 | 5 | let handleNotFound = function (data) { 6 | if (!data) { 7 | let err = new Error('Not Found') 8 | err.status = 404 9 | throw err 10 | } 11 | return data 12 | } 13 | 14 | // PRIVATE FN 15 | 16 | function DataSource(DataSourceModel) { 17 | this.model = DataSourceModel 18 | } 19 | 20 | DataSource.prototype.getAll = function (request, response, next) { 21 | this.model.findAll() 22 | .then(function (data) { 23 | response.json(data) 24 | }) 25 | .catch(next) 26 | } 27 | 28 | DataSource.prototype.getAllWithPlaces = function (request, response, next) { 29 | let _query = { 30 | include: [models.Local] 31 | } 32 | 33 | this.model.findAll(_query) 34 | .then(function (data) { 35 | response.json(data) 36 | }) 37 | .catch(next) 38 | } 39 | 40 | DataSource.prototype.getById = function (request, response, next) { 41 | let _query = { 42 | where: { id: request.params._id }, 43 | include: [models.Local] 44 | } 45 | 46 | this.model.find(_query) 47 | .then(handleNotFound) 48 | .then(function (data) { 49 | response.json(data) 50 | }) 51 | .catch(next) 52 | } 53 | 54 | DataSource.prototype.create = function (request, response, next) { 55 | let _body = request.body 56 | let _revision = { 57 | name: _body.name, 58 | url: _body.url 59 | } 60 | 61 | this.model.create(_revision) 62 | .then(function (data) { 63 | response.json(data) 64 | }) 65 | .catch(next) 66 | } 67 | 68 | DataSource.prototype.update = function (request, response, next) { 69 | let _id = request.params._id 70 | let _body = request.body 71 | let _revision = { 72 | url: _body.url, 73 | name: _body.name 74 | } 75 | let _query = { 76 | where: { id: _id } 77 | } 78 | 79 | this.model.find(_query) 80 | .then(handleNotFound) 81 | .then(function (data) { 82 | data.update(_revision) 83 | .then(function (tag) { 84 | response.json(tag) 85 | return tag 86 | }) 87 | .catch(next) 88 | return data 89 | }) 90 | .catch(next) 91 | } 92 | 93 | DataSource.prototype.remove = function (request, response, next) { 94 | let _id = request.params._id 95 | let _query = { 96 | where: { id: _id } 97 | } 98 | 99 | this.model.destroy(_query) 100 | .then(handleNotFound) 101 | .then(function (rowDeleted) { 102 | if (rowDeleted === 1) { 103 | response.json({ 104 | message: 'Deleted successfully' 105 | }) 106 | } 107 | }) 108 | .catch(next) 109 | } 110 | 111 | module.exports = function (DataSourceModel) { 112 | return new DataSource(DataSourceModel) 113 | } 114 | -------------------------------------------------------------------------------- /tests/unit/controllers/AuthController_test.js: -------------------------------------------------------------------------------- 1 | let request = require('supertest') 2 | let app = require('../../../app') 3 | let chai = require('chai') 4 | let expect = chai.expect 5 | 6 | describe('AuthController', function () { 7 | this.timeout(15000) 8 | let token = null 9 | 10 | before(function (done) { 11 | request(app) 12 | .post('/token') 13 | .send({ 14 | username: 'testuser', 15 | password: '123456' 16 | }) 17 | .end(function (err, res) { 18 | token = res.body.token 19 | done() 20 | }) 21 | }) 22 | 23 | describe('User', function () { 24 | it('delete all users', function (done) { 25 | request(app) 26 | .delete('/user') 27 | .set('x-access-token', token) 28 | .send() 29 | .expect(200) 30 | .end(function (err, res) { 31 | done(err) 32 | }) 33 | }) 34 | 35 | it('create user fake', function (done) { 36 | request(app) 37 | .post('/user') 38 | .set('x-access-token', token) 39 | .send({ 40 | fullname: 'Test Lastname', 41 | username: 'testuser', 42 | password: '123456', 43 | role: 'admin' 44 | }) 45 | .expect(200) 46 | .end(function (err, res) { 47 | done(err) 48 | }) 49 | }) 50 | }) 51 | 52 | describe('status 200', function () { 53 | it('returns authenticated user token', function (done) { 54 | request(app) 55 | .post('/token') 56 | .set('x-access-token', token) 57 | .send({ 58 | username: 'testuser', 59 | password: '123456' 60 | }) 61 | .expect(200) 62 | .end(function (err, res) { 63 | expect(res.body).to.include.keys('token') 64 | done(err) 65 | }) 66 | }) 67 | }) 68 | 69 | describe('status 400', function () { 70 | it('throws error when email and password are blank', function (done) { 71 | request(app) 72 | .post('/token') 73 | .set('x-access-token', token) 74 | .expect(400) 75 | .end(function (err, res) { 76 | done(err) 77 | }) 78 | }) 79 | }) 80 | 81 | describe('status 401', function () { 82 | it('throws error when password is incorrect', function (done) { 83 | request(app) 84 | .post('/token') 85 | .set('x-access-token', token) 86 | .send({ 87 | username: 'testuser', 88 | password: 'SENHA_ERRADA' 89 | }) 90 | .expect(401) 91 | .end(function (err, res) { 92 | done(err) 93 | }) 94 | }) 95 | }) 96 | 97 | describe('status 404', function () { 98 | it('throws error when email not exist', function (done) { 99 | request(app) 100 | .post('/token') 101 | .set('x-access-token', token) 102 | .send({ 103 | username: 'EMAIL_ERRADO', 104 | password: 'SENHA_ERRADA' 105 | }) 106 | .expect(404) 107 | .end(function (err, res) { 108 | done(err) 109 | }) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /public/stylesheets/all.min.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0}a{background:0 0;text-decoration:none}a:focus{outline:dotted thin}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}ol,ul{list-style:none}body{line-height:1;font:14px 'Open Sans',sans-serif;padding-top:70px;color:#333;min-width:300px}a:hover{text-decoration:underline}p{margin-bottom:20px}h1,h2,h3{font-weight:700;margin-bottom:15px}h1{font-size:1.5em}h2{font-size:1.2em}h3{font-size:1.1em}.fleft{float:left}#header{background-color:#034482;background-image:linear-gradient(to bottom,#323232,#222);width:100%;height:50px;line-height:50px;position:fixed;top:0;left:0}#header-title{color:#fff}.content{width:1180px;margin:0 auto}.api-item-title{color:#999;cursor:pointer;border-bottom:1px solid #ddd;padding-bottom:20px}.api-item-title:hover{color:#000}.endpoints{width:80%;margin:20px auto}.endpoint-item{margin-bottom:15px;border-radius:3px}.noun{color:#fff;border-radius:3px;text-align:center;width:60px;height:25px;line-height:25px;display:inline-block;font-size:.7em}.endpoint-heading{overflow:hidden}.endpoint-name{font-weight:700;margin-left:10px;padding-top:4px;color:#000;font-size:.9em}.noun-get{background-color:#E7F0F7;border:1px solid #c3d9ec}.noun-post{background-color:#E7F6EC;border:1px solid #c3e8d1}.noun-put{background-color:#f6e7f7;border:1px solid #dcaaf7}.noun-delete{background-color:#ffe1e1;border:1px solid #ee9898}.noun-get h3{color:#0F6AB4}.noun-post h3{color:#10A54A}.noun-put h3{color:#8320b7}.noun-delete h3{color:#dc2f2f}.noun-get .endpoint-heading{border-bottom:1px solid #c3d9ec}.noun-post .endpoint-heading{border-bottom:1px solid #c3e8d1}.noun-put .endpoint-heading{border-bottom:1px solid #dcaaf7}.noun-delete .endpoint-heading{border-bottom:1px solid #ee9898}.noun-get .noun{background-color:#0F6AB4}.noun-post .noun{background-color:#10A54A}.noun-put .noun{background-color:#8320b7}.noun-delete .noun{background-color:#dc2f2f}.endpoint-content{padding:15px}pre{white-space:pre-wrap;background:#F5F5F5;border:1px solid #DCDCDC;border-radius:3px;min-height:15px;padding:4px 15px}@media only screen and (max-width:1180px){body{font-size:12px}.content,.endpoints{width:95%}pre{overflow:auto;font-size:.9em}}@media only screen and (max-width:680px){pre{overflow:auto;font-size:.8em}} -------------------------------------------------------------------------------- /controllers/ReviewController.js: -------------------------------------------------------------------------------- 1 | let models = require('../models') 2 | let moment = require('moment') 3 | 4 | // PRIVATE FN 5 | 6 | let handleNotFound = function (data) { 7 | if (!data) { 8 | let err = new Error('Not Found') 9 | err.status = 404 10 | throw err 11 | } 12 | return data 13 | } 14 | 15 | let throwUnauthorizedError = function (next) { 16 | let err = new Error('Unauthorized') 17 | err.status = 401 18 | return next(err) 19 | } 20 | 21 | var promiseTags = function (tags) { 22 | return new Promise(function (resolve, reject) { 23 | var _promises = [] 24 | 25 | tags.map(function (tag) { 26 | _promises.push(models.Tag.find({where: {id: tag.id}})) 27 | }) 28 | 29 | Promise.all(_promises).then(function (tags) { 30 | resolve(tags) 31 | }) 32 | }) 33 | } 34 | 35 | // PRIVATE FN 36 | 37 | function ReviewController (ReviewModel) { 38 | this.model = ReviewModel 39 | } 40 | 41 | ReviewController.prototype.getAll = function (request, response, next) { 42 | let _query = { 43 | include: [models.Tag, models.Local, models.User] 44 | } 45 | 46 | this.model.findAll(_query) 47 | .then(function (data) { 48 | response.json(data) 49 | }) 50 | .catch(next) 51 | } 52 | 53 | ReviewController.prototype.getById = function (request, response, next) { 54 | var _query = { 55 | where: {id: request.params._id}, 56 | include: [models.Tag] 57 | } 58 | 59 | this.model.find(_query) 60 | .then(handleNotFound) 61 | .then(function (data) { 62 | response.json(data) 63 | }) 64 | .catch(next) 65 | } 66 | 67 | ReviewController.prototype.remove = function (request, response, next) { 68 | let _id = request.params._id 69 | let _query = { 70 | where: {id: _id} 71 | } 72 | 73 | // Check if user is logged in and has correct role 74 | const loggedUser = request.decoded; 75 | if (!loggedUser || loggedUser.role === 'client') { 76 | throwUnauthorizedError(next); 77 | } 78 | 79 | // If it's a regular user, check if he's the original creator 80 | if (loggedUser.role === 'user') { 81 | this.model.findOne({ where: { id: _id } }) 82 | .then(handleNotFound) 83 | .then(function (review) { 84 | if (review.user_id !== loggedUser.id) { 85 | throwUnauthorizedError(next); 86 | } 87 | }); 88 | } 89 | 90 | this.model.destroy(_query) 91 | .then(handleNotFound) 92 | .then(function (rowDeleted) { 93 | if (rowDeleted === 1) { 94 | response.json({ 95 | message: 'Deleted successfully' 96 | }) 97 | } 98 | }) 99 | .catch(next) 100 | } 101 | 102 | ReviewController.prototype._update = function (id, data) { 103 | const _query = { 104 | where: {id: id} 105 | } 106 | 107 | return new Promise(function (resolve, reject) { 108 | models.Review.find(_query) 109 | .then(handleNotFound) 110 | .then(function (r) { 111 | r.update(data) 112 | .then(function (review) { 113 | resolve(review) 114 | }) 115 | .catch( err => reject(err) ) 116 | resolve(r) 117 | }) 118 | .catch( err => reject(err) ) 119 | }) 120 | } 121 | 122 | ReviewController.prototype.update = function (request, response, next) { 123 | let _id = request.params._id 124 | let _body = request.body 125 | let _review = {} 126 | 127 | // Check if user is logged in and has correct role 128 | const loggedUser = request.decoded; 129 | if (!loggedUser || loggedUser.role === 'client') { 130 | throwUnauthorizedError(next); 131 | } 132 | 133 | // If it's a regular user, check if he's the original creator 134 | if (loggedUser.role === 'user') { 135 | this.model.findOne({ where: { id: _id } }) 136 | .then(handleNotFound) 137 | .then(function (review) { 138 | if (review.user_id !== loggedUser.id) { 139 | throwUnauthorizedError(next); 140 | } 141 | }); 142 | } 143 | 144 | if (_body.description) _review.description = _body.description 145 | if (_body.rating) _review.rating = _body.rating 146 | if (_body.user_id) _review.user_id = _body.user_id 147 | 148 | this._update(_id, _review) 149 | .then( review => { 150 | response.json(review) 151 | return review 152 | }) 153 | .catch(next) 154 | } 155 | 156 | ReviewController.prototype.create = function (request, response, next) { 157 | let _body = request.body 158 | let _self = this 159 | let _currentDate = moment().format('YYYY-MM-DD') 160 | let _currentHour = moment().format('YYYY-MM-DD HH:mm:ss') 161 | let _review = { 162 | description: _body.description, 163 | rating: _body.rating, 164 | hour: _currentHour, 165 | date: _currentDate, 166 | local_id: _body.idLocal 167 | } 168 | let _tags = _body.tags || [] 169 | 170 | // Check if user is logged in and has correct role 171 | const loggedUser = request.decoded; 172 | if (!loggedUser || loggedUser.role === 'client') { 173 | throwUnauthorizedError(next); 174 | } 175 | 176 | _review.user_id = loggedUser.id; 177 | 178 | promiseTags(_tags) 179 | .then(function (tagsResponse) { 180 | _self.model.create(_review) 181 | .then(function (review) { 182 | return review.setTags(tagsResponse) 183 | .then(function () { 184 | response.json(review) 185 | }) 186 | }) 187 | .catch(next) 188 | }) 189 | .catch(next) 190 | } 191 | 192 | module.exports = function (ReviewModel) { 193 | return new ReviewController(ReviewModel) 194 | } 195 | -------------------------------------------------------------------------------- /controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jwt-simple') 2 | const moment = require('moment') 3 | const models = require('../models') 4 | const request = require('request') 5 | 6 | const UserController = require('../controllers/UserController')(models.User) 7 | 8 | 9 | function AuthController (UserModel) { 10 | this.model = UserModel 11 | } 12 | 13 | AuthController.prototype.middlewareAuth = function (request, response, next) { 14 | var token = request.query.token || request.headers['x-access-token'] 15 | if (!token) { 16 | let err = new Error('Forbidden') 17 | err.status = 403 18 | return next(err) 19 | } 20 | try { 21 | let decoded = jwt.decode(token, process.env.JWT_TKN_SECRET) 22 | let isExpired = moment(decoded.exp).isBefore(new Date()) 23 | if (isExpired) { 24 | let err = new Error('Unauthorized') 25 | err.status = 401 26 | return next(err) 27 | } else { 28 | request.decoded = decoded 29 | next() 30 | } 31 | } catch (err) { 32 | return next(err) 33 | } 34 | } 35 | 36 | AuthController.prototype._validateWithProvider = function (network, socialToken) { 37 | var providers = { 38 | facebook: { 39 | url: 'https://graph.facebook.com/me' 40 | }, 41 | google: { 42 | url: 'https://www.googleapis.com/oauth2/v3/tokeninfo' 43 | } 44 | } 45 | 46 | return new Promise(function (resolve, reject) { 47 | request({ 48 | url: providers[network].url, 49 | qs: {access_token: socialToken} 50 | }, 51 | function (error, response, body) { 52 | if (!error && response.statusCode == 200) { 53 | resolve(JSON.parse(body)) 54 | } else { 55 | reject(error) 56 | } 57 | } 58 | ) 59 | }) 60 | } 61 | 62 | AuthController.prototype._generateJWT = function (foundUser, response, isNewUser=false) { 63 | let expires = moment().add(1, 'days').valueOf() 64 | let token = jwt.encode({ 65 | id: foundUser.id, 66 | username: foundUser.username || foundUser.name || '', 67 | role: foundUser.role, 68 | fullname: foundUser.fullname || '', 69 | exp: expires 70 | }, process.env.JWT_TKN_SECRET) 71 | 72 | response.json({ 73 | token: token, 74 | role: foundUser.role, 75 | isNewUser: isNewUser 76 | }) 77 | } 78 | 79 | // Social auth inspired by https://ole.michelsen.dk/blog/social-signin-spa-jwt-server.html 80 | AuthController.prototype.token = function (request, response, next) { 81 | const self = this; 82 | 83 | // Grab the social network and token 84 | let network = request.body.network 85 | let socialToken = request.body.socialToken 86 | let username = request.body.username 87 | let password = request.body.password 88 | let fullname = request.body.fullname 89 | let email = request.body.email 90 | 91 | if (! ((username && password) || 92 | (network && socialToken))) { 93 | let err = new Error('Bad request - missing network and/or socialToken.') 94 | err.status = 400 95 | return next(err) 96 | } 97 | 98 | if (network && socialToken) { 99 | // Login by social network 100 | // Fist, validate the social token. 101 | this._validateWithProvider(network, socialToken).then(function (profile) { 102 | console.log(profile); 103 | 104 | let query; 105 | switch (network) { 106 | case 'facebook': 107 | query = {facebook_id: profile.id} 108 | break; 109 | case 'google': 110 | // https://developers.google.com/identity/sign-in/web/backend-auth 111 | profile.id = profile.sub 112 | query = {google_id: profile.id} 113 | break; 114 | default: 115 | let err = new Error('Bad request') 116 | err.status = 400 117 | return next(err) 118 | } 119 | 120 | // Search in DB for user with that social profile ID 121 | self.model.findOne({ where: query }) 122 | .then(function (foundUser) { 123 | if (foundUser) { 124 | self._generateJWT(foundUser, response) 125 | } else { 126 | console.log('------CREATING NEW USER--------'); 127 | 128 | // Create new user with the social data 129 | const newUserData = { 130 | role: 'user', 131 | fullname: fullname, 132 | email: email 133 | }; 134 | 135 | switch (network) { 136 | case 'facebook': 137 | newUserData.facebook_id = profile.id; 138 | break; 139 | case 'google': 140 | newUserData.google_id = profile.id; 141 | break; 142 | } 143 | 144 | UserController.model.create(newUserData) 145 | .then(function (newUser) { 146 | self._generateJWT(newUser, response, true) 147 | }) 148 | .catch(next) 149 | } 150 | }) 151 | .catch(next) 152 | }).catch(next) 153 | } else { 154 | // Login by username + password 155 | this.model.findOne({ where: {username: username} }) 156 | .then(function (foundUser) { 157 | if (foundUser) { 158 | if (foundUser.validPassword(password, foundUser.password)) { 159 | self._generateJWT(foundUser, response) 160 | } else { 161 | let err = new Error('Unauthorized') 162 | err.status = 401 163 | next(err) 164 | } 165 | } else { 166 | let err = new Error('Login inexistent') 167 | err.status = 404 168 | next(err) 169 | } 170 | }) 171 | .catch(next) 172 | } 173 | 174 | } 175 | 176 | AuthController.prototype.middlewareLogging = function (request, response, next) { 177 | let fullUrl = request.protocol + '://' + request.get('host') + request.originalUrl 178 | let info = { 179 | user: request.decoded.fullname || request.decoded.username, 180 | role: request.decoded.role, 181 | endpoint: fullUrl, 182 | body: request.body, 183 | method: request.method, 184 | ip_origin: request.get('ip_origin') || '' 185 | } 186 | models.Log.create(info) 187 | next() 188 | } 189 | 190 | module.exports = function (UserModel) { 191 | return new AuthController(UserModel) 192 | } 193 | -------------------------------------------------------------------------------- /controllers/UserController.js: -------------------------------------------------------------------------------- 1 | let models = require('../models') 2 | let ReviewController = require('./ReviewController')(models.User) 3 | let LocalController = require('./LocalController')(models.User) 4 | 5 | // PRIVATE FN 6 | 7 | let handleNotFound = function (data) { 8 | if (!data) { 9 | let err = new Error('Not Found') 10 | err.status = 404 11 | throw err 12 | } 13 | return data 14 | } 15 | 16 | // PRIVATE FN 17 | 18 | function UserController (UserModel) { 19 | this.model = UserModel 20 | } 21 | 22 | UserController.prototype.getAll = function (request, response, next) { 23 | let _query = { 24 | attributes: {exclude: ['password']}, 25 | include: [ 26 | models.Local, 27 | { 28 | model: models.Review, 29 | include: [models.Tag] 30 | } 31 | ] 32 | } 33 | 34 | this.model.findAll(_query) 35 | .then(function (data) { 36 | response.json(data) 37 | }) 38 | .catch(next) 39 | } 40 | 41 | UserController.prototype.getById = function (request, response, next) { 42 | let _query = { 43 | where: {id: request.params._id}, 44 | attributes: {exclude: ['password']}, 45 | // include: [models.Review] 46 | } 47 | 48 | this.model.find(_query) 49 | .then(handleNotFound) 50 | .then(function (data) { 51 | response.json(data) 52 | }) 53 | .catch(next) 54 | } 55 | 56 | UserController.prototype.importReviewsToCurrentUser = function (request, response, next) { 57 | const _body = request.body 58 | const reviews = _body.reviews; 59 | if (!reviews) { 60 | let err = new Error('No reviews found in request body.') 61 | err.status = 400 62 | throw err 63 | } 64 | 65 | // Check if we do have a logged user 66 | const currentUser = request.decoded; 67 | if (currentUser.role === 'client') { 68 | let err = new Error('No logged user.') 69 | err.status = 400 70 | throw err 71 | } 72 | 73 | // Collect promises for all reviews' updates 74 | let updatesPromises = []; 75 | let numImports = 0; 76 | reviews.forEach(r => { 77 | if (!r.databaseId) { 78 | let err = new Error('Review without ID') 79 | err.status = 400 80 | throw err 81 | } else { 82 | updatesPromises.push( 83 | ReviewController._update.bind(ReviewController)( 84 | r.databaseId, 85 | {user_id: currentUser.id} 86 | ).then(() => { 87 | numImports++; 88 | }).catch(err => { 89 | console.log(err); 90 | 91 | // Don't throw error, just let it be. 92 | // let throwErr = new Error(`Error updating review ${r.databaseId}`) 93 | // throwErr.status = 500 94 | // throw throwErr 95 | }) 96 | ); 97 | } 98 | }); 99 | 100 | // Wait until all updates are done 101 | Promise.all(updatesPromises).then(() => { 102 | response.json({ 103 | numImports: numImports, 104 | message: `${numImports} reviews imported successfully.` 105 | }); 106 | }).catch(next) 107 | } 108 | 109 | UserController.prototype.importLocalsToCurrentUser = function (request, response, next) { 110 | const _body = request.body 111 | const locals = _body.locals; 112 | if (!locals) { 113 | let err = new Error('No locals found in request body.') 114 | err.status = 404 115 | throw err 116 | } 117 | 118 | // Check if we do have a logged user 119 | const currentUser = request.decoded; 120 | if (currentUser.role === 'client') { 121 | let err = new Error('No logged user.') 122 | err.status = 404 123 | throw err 124 | } 125 | 126 | // Collect promises for all locals' updates 127 | let updatesPromises = []; 128 | locals.forEach(local => { 129 | updatesPromises.push( 130 | LocalController._update.bind(LocalController)( 131 | local.id, 132 | {user_id: currentUser.id} 133 | ).catch(error => { 134 | let err = new Error('Something went wrong when updating local ' + local.id + error) 135 | err.status = 404 136 | throw err 137 | }) 138 | ); 139 | }); 140 | 141 | // Wait until all updates are done 142 | Promise.all(updatesPromises).then(() => { 143 | response.json({ 144 | message: `${locals.length} locals imported successfully.` 145 | }); 146 | }).catch(next) 147 | } 148 | 149 | UserController.prototype.getCurrentUserLocals = function (request, response, next) { 150 | const currentUser = request.decoded; 151 | 152 | if (currentUser.role === 'client') { 153 | let err = new Error('No logged user.') 154 | err.status = 404 155 | throw err 156 | } 157 | 158 | this.getUserLocals({params: {_id: currentUser.id}}, response, next); 159 | } 160 | 161 | UserController.prototype.getUserLocals = function (request, response, next) { 162 | let _id = request.params._id 163 | 164 | let _query = { 165 | where: {id: _id}, 166 | attributes: {exclude: ['password']}, 167 | include: [models.Local] 168 | } 169 | 170 | this.model.find(_query) 171 | .then(handleNotFound) 172 | .then(function (data) { 173 | response.json(data) 174 | }) 175 | .catch(next) 176 | } 177 | 178 | UserController.prototype.getCurrentUserReviews = function (request, response, next) { 179 | const currentUser = request.decoded; 180 | 181 | if (currentUser.role === 'client') { 182 | let err = new Error('No logged user.') 183 | err.status = 400 184 | throw err 185 | } 186 | 187 | this.getUserReviews({params: {_id: currentUser.id}}, response, next); 188 | } 189 | 190 | UserController.prototype.getUserReviews = function (request, response, next) { 191 | let _id = request.params._id 192 | 193 | let _query = { 194 | where: {id: _id}, 195 | attributes: {exclude: ['password']}, 196 | include: [ 197 | { 198 | model: models.Review, 199 | include: [models.Tag, models.Local] 200 | }, 201 | ] 202 | } 203 | 204 | this.model.find(_query) 205 | .then(handleNotFound) 206 | .then(function (data) { 207 | response.json(data) 208 | }) 209 | .catch(next) 210 | } 211 | 212 | UserController.prototype.getCurrentUser = function (request, response, next) { 213 | const currentUser = request.decoded; 214 | 215 | if (currentUser.role === 'client') { 216 | let err = new Error('No logged user.') 217 | err.status = 404 218 | throw err 219 | } 220 | 221 | let _query = { 222 | where: {id: currentUser.id}, 223 | attributes: {exclude: ['password']}, 224 | include: [models.Local, models.Review], 225 | } 226 | 227 | this.model.find(_query) 228 | .then(handleNotFound) 229 | .then(function (data) { 230 | response.json(data) 231 | }) 232 | .catch(next) 233 | } 234 | 235 | UserController.prototype.create = function (request, response, next) { 236 | let _body = request.body 237 | let _user = { 238 | fullname: _body.fullname, 239 | username: _body.username, 240 | password: _body.password, 241 | role: _body.role 242 | } 243 | 244 | if (_body.facebook_id) _user.facebook_id = _body.facebook_id 245 | if (_body.google_id) _user.google_id = _body.google_id 246 | if (_body.email) _user.email = _body.email 247 | 248 | this.model.create(_user) 249 | .then(function (data) { 250 | response.json(data) 251 | }) 252 | .catch(next) 253 | } 254 | 255 | UserController.prototype.update = function (request, response, next) { 256 | let _id = request.params._id 257 | let _body = request.body 258 | let _user = {} 259 | let _query = { 260 | where: {id: _id} 261 | } 262 | 263 | if (_body.fullname) _user.fullname = _body.fullname 264 | if (_body.username) _user.username = _body.username 265 | if (_body.password) _user.password = _body.password 266 | if (_body.role) _user.role = _body.role 267 | if (_body.facebook_id) _user.facebook_id = _body.facebook_id 268 | if (_body.google_id) _user.google_id = _body.google_id 269 | if (_body.email) _user.email = _body.email 270 | 271 | this.model.find(_query) 272 | .then(handleNotFound) 273 | .then(function (data) { 274 | data.update(_user) 275 | .then(function (user) { 276 | response.json(user) 277 | return user 278 | }) 279 | .catch(next) 280 | return data 281 | }) 282 | .catch(next) 283 | } 284 | 285 | UserController.prototype.remove = function (request, response, next) { 286 | let _id = request.params._id 287 | let _query = { 288 | where: {id: _id} 289 | } 290 | 291 | this.model.destroy(_query) 292 | .then(handleNotFound) 293 | .then(function (rowDeleted) { 294 | if (rowDeleted === 1) { 295 | response.json({ 296 | message: 'Deleted successfully' 297 | }) 298 | } 299 | }) 300 | .catch(next) 301 | } 302 | 303 | UserController.prototype.removeAll = function (request, response, next) { 304 | let _query = { 305 | where: {} 306 | } 307 | 308 | this.model.destroy(_query) 309 | .then(handleNotFound) 310 | .then(function (data) { 311 | response.json({ 312 | message: 'Deleted successfully' 313 | }) 314 | }) 315 | .catch(next) 316 | } 317 | 318 | module.exports = function (UserModel) { 319 | return new UserController(UserModel) 320 | } 321 | -------------------------------------------------------------------------------- /src/stylesheets/normaset.css: -------------------------------------------------------------------------------- 1 | /*======================================================== 2 | = Normaset.css V1.0 | MIT License | git.io/normalize = 3 | ========================================================*/ 4 | /* 5 | * 6 | * The purpose of this library is to normalize and clear all html 7 | * elements in different browsers, to accomplish this goal, 8 | * the Normaset was based on normalize.css and reset.css. 9 | * 10 | */ 11 | /*! Based on: 12 | /*! normalize.css v2.1.3 | MIT License | git.io/normalize */ 13 | /*! http://meyerweb.com/eric/tools/css/reset/ | v2.0 | 20110126 | License: none (public domain) */ 14 | /* ========================================================================== 15 | HTML5 display definitions 16 | ========================================================================== */ 17 | /** 18 | * Correct `block` display not defined in IE 8/9. 19 | */ 20 | article, 21 | aside, 22 | details, 23 | figcaption, 24 | figure, 25 | footer, 26 | header, 27 | hgroup, 28 | menu, 29 | main, 30 | nav, 31 | section, 32 | summary { 33 | display: block; 34 | } 35 | /** 36 | * Correct `inline-block` display not defined in IE 8/9. 37 | */ 38 | audio, 39 | canvas, 40 | video { 41 | display: inline-block; 42 | } 43 | /** 44 | * Prevent modern browsers from displaying `audio` without controls. 45 | * Remove excess height in iOS 5 devices. 46 | */ 47 | audio:not([controls]) { 48 | display: none; 49 | height: 0; 50 | } 51 | /** 52 | * Address `[hidden]` styling not present in IE 8/9. 53 | * Hide the `template` element in IE, Safari, and Firefox < 22. 54 | */ 55 | [hidden], 56 | template { 57 | display: none; 58 | } 59 | /* ========================================================================== 60 | Base 61 | ========================================================================== */ 62 | /** 63 | * 1. Set default font family to sans-serif. 64 | * 2. Prevent iOS text size adjust after orientation change, without disabling 65 | * user zoom. 66 | */ 67 | html { 68 | font-family: sans-serif; 69 | /* 1 */ 70 | 71 | -ms-text-size-adjust: 100%; 72 | /* 2 */ 73 | 74 | -webkit-text-size-adjust: 100%; 75 | /* 2 */ 76 | 77 | } 78 | /** 79 | * Remove default margin. 80 | */ 81 | html, 82 | body, 83 | div, 84 | span, 85 | applet, 86 | object, 87 | iframe, 88 | h1, 89 | h2, 90 | h3, 91 | h4, 92 | h5, 93 | h6, 94 | p, 95 | blockquote, 96 | pre, 97 | a, 98 | abbr, 99 | acronym, 100 | address, 101 | big, 102 | cite, 103 | code, 104 | del, 105 | dfn, 106 | em, 107 | img, 108 | ins, 109 | kbd, 110 | q, 111 | s, 112 | samp, 113 | small, 114 | strike, 115 | strong, 116 | sub, 117 | sup, 118 | tt, 119 | var, 120 | b, 121 | u, 122 | i, 123 | center, 124 | dl, 125 | dt, 126 | dd, 127 | ol, 128 | ul, 129 | li, 130 | form, 131 | label, 132 | legend, 133 | table, 134 | caption, 135 | tbody, 136 | tfoot, 137 | thead, 138 | tr, 139 | th, 140 | td, 141 | article, 142 | aside, 143 | canvas, 144 | details, 145 | embed, 146 | figure, 147 | figcaption, 148 | footer, 149 | header, 150 | hgroup, 151 | menu, 152 | nav, 153 | output, 154 | ruby, 155 | section, 156 | summary, 157 | time, 158 | mark, 159 | audio, 160 | video { 161 | margin: 0; 162 | padding: 0; 163 | border: 0; 164 | } 165 | body { 166 | line-height: 1; 167 | } 168 | /* ========================================================================== 169 | Links 170 | ========================================================================== */ 171 | /** 172 | * Remove the gray background color from active links in IE 10. 173 | */ 174 | a { 175 | background: transparent; 176 | } 177 | /** 178 | * Address `outline` inconsistency between Chrome and other browsers. 179 | */ 180 | a:focus { 181 | outline: thin dotted; 182 | } 183 | /** 184 | * Improve readability when focused and also mouse hovered in all browsers. 185 | */ 186 | a:active, 187 | a:hover { 188 | outline: 0; 189 | } 190 | /* ========================================================================== 191 | Typography 192 | ========================================================================== */ 193 | /** 194 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 195 | */ 196 | abbr[title] { 197 | border-bottom: 1px dotted; 198 | } 199 | /** 200 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 201 | */ 202 | b, 203 | strong { 204 | font-weight: bold; 205 | } 206 | /** 207 | * Address styling not present in Safari 5 and Chrome. 208 | */ 209 | dfn { 210 | font-style: italic; 211 | } 212 | /** 213 | * Address differences between Firefox and other browsers. 214 | */ 215 | hr { 216 | -moz-box-sizing: content-box; 217 | box-sizing: content-box; 218 | height: 0; 219 | } 220 | /** 221 | * Address styling not present in IE 8/9. 222 | */ 223 | mark { 224 | background: #ff0; 225 | color: #000; 226 | } 227 | /** 228 | * Correct font family set oddly in Safari 5 and Chrome. 229 | */ 230 | code, 231 | kbd, 232 | pre, 233 | samp { 234 | font-family: monospace, serif; 235 | font-size: 1em; 236 | } 237 | /** 238 | * Improve readability of pre-formatted text in all browsers. 239 | */ 240 | pre { 241 | white-space: pre-wrap; 242 | } 243 | /** 244 | * Set consistent quote types. 245 | */ 246 | blockquote, 247 | q { 248 | quotes: none; 249 | } 250 | blockquote:before, 251 | blockquote:after, 252 | q:before, 253 | q:after { 254 | content: ''; 255 | content: none; 256 | } 257 | /** 258 | * Address inconsistent and variable font size in all browsers. 259 | */ 260 | small { 261 | font-size: 80%; 262 | } 263 | /** 264 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 265 | */ 266 | sub, 267 | sup { 268 | font-size: 75%; 269 | line-height: 0; 270 | position: relative; 271 | vertical-align: baseline; 272 | } 273 | sup { 274 | top: -0.5em; 275 | } 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | /* ========================================================================== 280 | Embedded content 281 | ========================================================================== */ 282 | /** 283 | * Correct overflow displayed oddly in IE 9. 284 | */ 285 | svg:not(:root) { 286 | overflow: hidden; 287 | } 288 | /* ========================================================================== 289 | Forms 290 | ========================================================================== */ 291 | /** 292 | * Define consistent border, margin, and padding. 293 | */ 294 | fieldset { 295 | border: 1px solid #c0c0c0; 296 | margin: 0 2px; 297 | padding: 0.35em 0.625em 0.75em; 298 | } 299 | /** 300 | * 1. Correct font family not being inherited in all browsers. 301 | * 2. Correct font size not being inherited in all browsers. 302 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 303 | */ 304 | button, 305 | input, 306 | select, 307 | textarea { 308 | font-family: inherit; 309 | /* 1 */ 310 | 311 | font-size: 100%; 312 | /* 2 */ 313 | 314 | margin: 0; 315 | /* 3 */ 316 | 317 | } 318 | /** 319 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 320 | * the UA stylesheet. 321 | */ 322 | button, 323 | input { 324 | line-height: normal; 325 | } 326 | /** 327 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 328 | * All other form control elements do not inherit `text-transform` values. 329 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 330 | * Correct `select` style inheritance in Firefox 4+ and Opera. 331 | */ 332 | button, 333 | select { 334 | text-transform: none; 335 | } 336 | /** 337 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 338 | * and `video` controls. 339 | * 2. Correct inability to style clickable `input` types in iOS. 340 | * 3. Improve usability and consistency of cursor style between image-type 341 | * `input` and others. 342 | */ 343 | button, 344 | html input[type="button"], 345 | input[type="reset"], 346 | input[type="submit"] { 347 | -webkit-appearance: button; 348 | /* 2 */ 349 | 350 | cursor: pointer; 351 | /* 3 */ 352 | 353 | } 354 | /** 355 | * Re-set default cursor for disabled elements. 356 | */ 357 | button[disabled], 358 | html input[disabled] { 359 | cursor: default; 360 | } 361 | /** 362 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 363 | * 2. Remove excess padding in IE 8/9/10. 364 | */ 365 | input[type="checkbox"], 366 | input[type="radio"] { 367 | box-sizing: border-box; 368 | /* 1 */ 369 | 370 | padding: 0; 371 | /* 2 */ 372 | 373 | } 374 | /** 375 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 376 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 377 | * (include `-moz` to future-proof). 378 | */ 379 | input[type="search"] { 380 | -webkit-appearance: textfield; 381 | /* 1 */ 382 | 383 | -moz-box-sizing: content-box; 384 | -webkit-box-sizing: content-box; 385 | /* 2 */ 386 | 387 | box-sizing: content-box; 388 | } 389 | /** 390 | * Remove inner padding and search cancel button in Safari 5 and Chrome 391 | * on OS X. 392 | */ 393 | input[type="search"]::-webkit-search-cancel-button, 394 | input[type="search"]::-webkit-search-decoration { 395 | -webkit-appearance: none; 396 | } 397 | /** 398 | * Remove inner padding and border in Firefox 4+. 399 | */ 400 | button::-moz-focus-inner, 401 | input::-moz-focus-inner { 402 | border: 0; 403 | padding: 0; 404 | } 405 | /** 406 | * 1. Remove default vertical scrollbar in IE 8/9. 407 | * 2. Improve readability and alignment in all browsers. 408 | */ 409 | textarea { 410 | overflow: auto; 411 | /* 1 */ 412 | 413 | vertical-align: top; 414 | /* 2 */ 415 | 416 | } 417 | /* ========================================================================== 418 | Tables 419 | ========================================================================== */ 420 | /** 421 | * Remove most spacing between table cells. 422 | */ 423 | table { 424 | border-collapse: collapse; 425 | border-spacing: 0; 426 | } 427 | /* ========================================================================== 428 | Lists 429 | ========================================================================== */ 430 | /** 431 | * 432 | * removing spaces and wiping style of elements 433 | * 434 | **/ 435 | ol, 436 | ul { 437 | list-style: none; 438 | } 439 | -------------------------------------------------------------------------------- /controllers/LocalController.js: -------------------------------------------------------------------------------- 1 | let debug = require('debug')('api:ctrlLocal') 2 | let models = require('../models') 3 | let AWS = require('aws-sdk') 4 | let sharp = require('sharp') 5 | const env = process.env.NODE_ENV || 'development' 6 | //const AWS_PATH_PREFIX = (env === 'development') ? 'https://s3.amazonaws.com/bikedeboa-dev/' : 'https://s3.amazonaws.com/bikedeboa/' 7 | //const BUCKET_NAME = (env === 'development') ? 'bikedeboa-dev' : 'bikedeboa'; 8 | 9 | const AWS_PATH_PREFIX = process.env.AWS_PATH_PREFIX || 'https://s3.amazonaws.com/bikedeboa-dev/'; 10 | const BUCKET_NAME = process.env.BUCKET_NAME || 'bikedeboa-dev'; 11 | 12 | console.log('AWS_PATH_PREFIX', AWS_PATH_PREFIX); 13 | console.log('BUCKET_NAME', BUCKET_NAME); 14 | 15 | let s3 = new AWS.S3() 16 | 17 | // PRIVATE FN // 18 | 19 | let handleNotFound = function (data) { 20 | if (!data) { 21 | let err = new Error('Not Found') 22 | err.status = 404 23 | throw err 24 | } 25 | return data 26 | } 27 | 28 | let contTagsLocal = function (local) { 29 | return new Promise(function (resolve, reject) { 30 | models.sequelize.query('SELECT t.name, COUNT(*) FROM "Tag" t inner join "Review_Tags" rt on T.id = rt.tag_id inner join "Review" r on r.id = rt.review_id inner join "Local" l on r.local_id = l.id WHERE l.id = ' + local.id + ' GROUP BY t.id') 31 | .then(function (result, metatag) { 32 | local.dataValues.tags = result[0] 33 | resolve(local) 34 | }) 35 | }) 36 | } 37 | 38 | let throwUnauthorizedError = function (next) { 39 | let err = new Error('Unauthorized') 40 | err.status = 401 41 | return next(err) 42 | } 43 | 44 | var saveFullImage = function (params) { 45 | return new Promise(function (resolve, reject) { 46 | // params 47 | let _photo = params.photo 48 | let _id = params.id 49 | 50 | // valid photo exists 51 | if (!_photo) resolve('') 52 | 53 | // get base64 and type image for save 54 | let type = _photo.split(',')[0] === 'data:image/png;base64' ? '.png' : _photo.split(',')[0] === 'data:image/jpeg;base64' ? '.jpeg' : '' 55 | let base64Data = type === '.png' ? _photo.replace(/^data:image\/png;base64,/, '') : _photo.replace(/^data:image\/jpeg;base64,/, '') 56 | base64Data += base64Data.replace('+', ' ') 57 | let binaryData = new Buffer(base64Data, 'base64') 58 | 59 | // path image 60 | let path = 'images/' 61 | let imageName = path + _id + '-' + params.timestamp + type 62 | 63 | // type invalid return 64 | if (!type) { 65 | reject(_photo) 66 | } 67 | 68 | // Send image blob to Amazon S3 69 | s3.putObject( 70 | { 71 | Key: imageName, 72 | Body: binaryData, 73 | Bucket: BUCKET_NAME, 74 | ACL: 'public-read' 75 | }, function (err, data) { 76 | if (err) { 77 | debug('Error uploading image ', imageName) 78 | reject(err) 79 | } else { 80 | debug('Succesfully uploaded the image', imageName) 81 | resolve(AWS_PATH_PREFIX + imageName) 82 | } 83 | }) 84 | }) 85 | } 86 | 87 | var saveThumbImage = function (params) { 88 | return new Promise(function (resolve, reject) { 89 | // params 90 | let _photo = params.photo 91 | let _id = params.id 92 | 93 | // valid photo exists 94 | if (!_photo) resolve('') 95 | 96 | // get base64 and type image for save 97 | let type = _photo.split(',')[0] === 'data:image/png;base64' ? '.png' : _photo.split(',')[0] === 'data:image/jpeg;base64' ? '.jpeg' : '' 98 | let base64Data = type === '.png' ? _photo.replace(/^data:image\/png;base64,/, '') : _photo.replace(/^data:image\/jpeg;base64,/, '') 99 | base64Data += base64Data.replace('+', ' ') 100 | let binaryData = new Buffer(base64Data, 'base64') 101 | 102 | // path image 103 | let path = 'images/thumbs/' 104 | let imageName = path + _id + '-' + params.timestamp + type 105 | 106 | // type invalid return 107 | if (!type) { 108 | reject(_photo) 109 | } 110 | 111 | sharp(binaryData) 112 | .resize(100, 100) 113 | .max() 114 | .on('error', function (err) { 115 | reject(err) 116 | }) 117 | .toBuffer() 118 | .then(function (data) { 119 | // Send image blob to Amazon S3 120 | s3.putObject( 121 | { 122 | Key: imageName, 123 | Body: data, 124 | Bucket: BUCKET_NAME, 125 | ACL: 'public-read' 126 | }, function (err, data) { 127 | if (err) { 128 | reject(err) 129 | console.log("Erro ao salvar imagem no S3", err) 130 | } else { 131 | console.log("Imagem salva com sucesso", imageName) 132 | resolve(AWS_PATH_PREFIX + imageName) 133 | } 134 | }) 135 | }) 136 | }) 137 | } 138 | 139 | var deleteImage = function (name) { 140 | return new Promise(function (resolve, reject) { 141 | // valid photo 142 | if (!name || name === '') resolve('') 143 | 144 | // path image 145 | let path = 'images/' 146 | let pathThumb = 'images/thumbs/' 147 | 148 | let imageName = path + name 149 | let imageNameTumb = pathThumb + name 150 | 151 | // params delete images 152 | let params = { 153 | Bucket: BUCKET_NAME, 154 | Delete: { 155 | Objects: [ 156 | { 157 | Key: imageName 158 | }, 159 | { 160 | Key: imageNameTumb 161 | } 162 | ] 163 | } 164 | } 165 | // delete imagens in s3 166 | s3.deleteObjects(params, function (err, data) { 167 | if (err) { 168 | reject(err) 169 | } else { 170 | resolve(data) 171 | } 172 | }) 173 | }) 174 | } 175 | 176 | // PRIVATE FN // 177 | 178 | function LocalController (LocalModel) { 179 | this.model = LocalModel 180 | } 181 | 182 | LocalController.prototype.getAll = function (request, response, next) { 183 | var _query = { 184 | attributes: ['id', 'lat', 'lng', 'lat', 'structureType', 'isPublic', 'isCovered', 'text', 'description', 'address', 'photo', 'updatedAt', 'createdAt', 'views', 'city', 'state', 'country', 'isPaid', 'slots'].concat([ 185 | [ 186 | models.sequelize.literal('(SELECT COUNT(*) FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 187 | 'reviews' 188 | ], 189 | [ 190 | models.sequelize.literal('(SELECT AVG("rating") FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 191 | 'average' 192 | ] 193 | ]), 194 | include: [{ 195 | model: models.User, 196 | attributes: ['fullname'] 197 | }, { 198 | model: models.Review, 199 | include: [models.Tag] 200 | }, { 201 | model: models.DataSource 202 | }] 203 | } 204 | 205 | this.model.findAll(_query) 206 | .then(function (locals) { 207 | response.json(locals) 208 | }) 209 | .catch(next) 210 | } 211 | 212 | LocalController.prototype.getAllLight = function (request, response, next) { 213 | var _query = { 214 | attributes: ['id', 'lat', 'lng', 'isPublic', 'isCovered', 'structureType', 'text', 'photo', 'address', 'city', 'state', 'country'].concat([ 215 | [ 216 | models.sequelize.literal('(SELECT COUNT(*) FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 217 | 'reviews' 218 | ], 219 | [ 220 | models.sequelize.literal('(SELECT AVG("rating") FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 221 | 'average' 222 | ] 223 | ]) 224 | } 225 | 226 | this.model.findAll(_query) 227 | .then(function (locals) { 228 | response.json(locals) 229 | }) 230 | .catch(next) 231 | } 232 | 233 | LocalController.prototype.getById = function (request, response, next) { 234 | var self = this; 235 | 236 | // @todo refactoring... 237 | // https://github.com/sequelize/sequelize/issues/222 238 | // var _query = { 239 | // attributes: 240 | // 'Local.*',// 'Review.*' 241 | // // [models.sequelize.fn('COUNT', '*'), 'reviews'], 242 | // // [models.sequelize.fn('AVERAGE', 'rating'), 'average'] 243 | // [ 244 | // models.sequelize.literal('(SELECT COUNT(*) FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 245 | // 'reviews' 246 | // ], 247 | // [ 248 | // models.sequelize.literal('(SELECT AVG("rating") FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 249 | // 'average' 250 | // ] 251 | 252 | var _query = { 253 | attributes: ['id', 'lat', 'lng', 'lat', 'structureType', 'isPublic', 'isCovered', 'text', 'photo', 'description', 'address', 'createdAt', 'views', 'city', 'state', 'country', 'isPaid', 'slots'].concat([ 254 | [ 255 | models.sequelize.literal('(SELECT COUNT(*) FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 256 | 'reviews' 257 | ], 258 | [ 259 | models.sequelize.literal('(SELECT AVG("rating") FROM "Review" WHERE "Review"."local_id" = "Local"."id")'), 260 | 'average' 261 | ] 262 | ]), 263 | where: {id: request.params._id}, 264 | include: [{ 265 | model: models.User, 266 | attributes: ['fullname'] 267 | }, { 268 | model: models.DataSource 269 | }], 270 | } 271 | 272 | this.model.find(_query) 273 | .then(handleNotFound) 274 | .then(contTagsLocal) 275 | .then(function (local) { 276 | self._update(request.params._id, {views: local.dataValues.views+1}) 277 | 278 | const loggedUser = request.decoded; 279 | local.dataValues.wasCreatedByLoggedUser = !!(loggedUser && (local.user_id === loggedUser.id)); 280 | local.dataValues.canLoggedUserDelete = !!(local.dataValues.wasCreatedByLoggedUser); 281 | 282 | response.json(local) 283 | }) 284 | .catch(next) 285 | } 286 | 287 | LocalController.prototype.create = function (request, response, next) { 288 | var _body = request.body 289 | var _params = { 290 | lat: _body.lat, 291 | lng: _body.lng, 292 | text: _body.text, 293 | photo: '', 294 | description: _body.description, 295 | address: _body.address, 296 | authorIP: _body.authorIP 297 | } 298 | var isAnonymous = _body.isAnonymous; 299 | let timestamp = new Date().getTime() 300 | 301 | // Save author user if there's one authenticated 302 | // Obs: the 'client' role is the a regular, authenticated web client, but not a logged in user 303 | const loggedUser = request.decoded; 304 | if (loggedUser && loggedUser.role !== 'client') { 305 | if (!isAnonymous) { 306 | _params.user_id = loggedUser.id; 307 | } 308 | } else { 309 | throwUnauthorizedError(next); 310 | } 311 | 312 | if (_body.structureType) _params.structureType = _body.structureType 313 | if (_body.isPublic) _params.isPublic = _body.isPublic && (_body.isPublic === 'true' ? 1 : 0) 314 | if (_body.isCovered) _params.isCovered = _body.isCovered && (_body.isCovered === 'true' ? 1 : 0) 315 | if (_body.city) _params.city = _body.city 316 | if (_body.state) _params.state = _body.state 317 | if (_body.country) _params.country = _body.country 318 | if (_body.slots) _params.slots = _body.slots 319 | if (_body.isPaid) _params.isPaid = _body.isPaid 320 | if (_body.datasource_id) _params.datasource_id = _body.datasource_id 321 | 322 | var _local = {} 323 | 324 | this.model.create(_params) 325 | .then(function (local) { 326 | _local = local 327 | return {photo: _body.photo, id: _local.id, timestamp: timestamp} 328 | }) 329 | .then(saveThumbImage) 330 | .then(function (url) { 331 | return {photo: _body.photo, id: _local.id, timestamp: timestamp} 332 | }) 333 | .then(saveFullImage) 334 | .then(function (url) { 335 | return {photo: url} 336 | }) 337 | .then(function (url) { 338 | return _local.update(url) 339 | }) 340 | .then(function (local) { 341 | _local.photo = local.photo 342 | response.json(_local) 343 | }) 344 | .catch(next) 345 | } 346 | 347 | LocalController.prototype._update = function (id, data, photo, silentEdit = false) { 348 | let query = { 349 | where: {id: id} 350 | } 351 | let timestamp = new Date().getTime() 352 | 353 | // If we're just updating the views count we don't touch the updatedAt field 354 | if (Object.keys(data).length === 1 && data.views) { 355 | silentEdit = true; 356 | } 357 | 358 | return new Promise(function (resolve, reject) { 359 | models.Local.find(query) 360 | .then(handleNotFound) 361 | .then(function (local) { 362 | return local.update(data, { silent: silentEdit }) 363 | }) 364 | .then(function (local) { 365 | data = local 366 | if (photo) { 367 | return deleteImage(local.photo) 368 | } else { 369 | return data 370 | } 371 | }) 372 | .then(function (local) { 373 | if (photo) { 374 | return saveThumbImage({photo: photo, id: id, timestamp: timestamp}) 375 | } else { 376 | return local 377 | } 378 | }) 379 | .then(function (local) { 380 | if (photo) { 381 | return saveFullImage({photo: photo, id: id, timestamp: timestamp}) 382 | } else { 383 | return undefined 384 | } 385 | }) 386 | .then(function (url) { 387 | if (url) { 388 | return data.update({photo: url}) 389 | } else { 390 | return url 391 | } 392 | }) 393 | .then(function (resp) { 394 | if (typeof resp === 'string') { 395 | data.photo = resp 396 | } else { 397 | return data 398 | } 399 | }) 400 | .then(resolve) 401 | .catch( err => reject(err) ) 402 | }); 403 | } 404 | 405 | LocalController.prototype.update = function (request, response, next) { 406 | const _id = request.params._id 407 | const _body = request.body 408 | let _local = {} 409 | 410 | // Check if user is logged in and has correct role 411 | const loggedUser = request.decoded; 412 | if (!loggedUser || loggedUser.role === 'client') { 413 | throwUnauthorizedError(next); 414 | } 415 | 416 | _local.description = _body.description 417 | 418 | let silentEdit = false; 419 | if (_body.silentEdit && loggedUser.role === 'admin') { 420 | silentEdit = true; 421 | } 422 | 423 | if (_body.lat) _local.lat = _body.lat 424 | if (_body.lng) _local.lng = _body.lng 425 | 426 | if (_body.structureType) _local.structureType = _body.structureType 427 | if (_body.isPublic) _local.isPublic = _body.isPublic && (_body.isPublic === 'true' ? 1 : 0) 428 | if (_body.isCovered) _local.isCovered = _body.isCovered && (_body.isCovered === 'true' ? 1 : 0) 429 | if (_body.text) _local.text = _body.text 430 | if (_body.address) _local.address = _body.address 431 | if (_body.photoUrl) _local.photo = _body.photoUrl 432 | if (_body.views) _local.views = _body.views 433 | if (_body.city) _local.city = _body.city 434 | if (_body.state) _local.state = _body.state 435 | if (_body.country) _local.country = _body.country 436 | if (_body.slots) _local.slots = _body.slots 437 | if (_body.isPaid) _local.isPaid = _body.isPaid 438 | if (_body.datasource_id) _local.datasource_id = _body.datasource_id 439 | 440 | this._update(_id, _local, _body.photo, silentEdit) 441 | .then( local => { 442 | response.json(local) 443 | return local 444 | }) 445 | .catch(next) 446 | } 447 | 448 | LocalController.prototype.remove = function (request, response, next) { 449 | let _id = request.params._id 450 | let _query = { 451 | where: {id: _id} 452 | } 453 | let placeToDelete; 454 | 455 | // Check if user is logged in and has correct role 456 | const loggedUser = request.decoded; 457 | if (!loggedUser || loggedUser.role === 'client') { 458 | throwUnauthorizedError(next); 459 | } 460 | 461 | this.model.findOne(_query) 462 | .then(handleNotFound) 463 | .then(function (data) { 464 | placeToDelete = data; 465 | 466 | // If it's a normal user, check if he's the place creator 467 | if (loggedUser.role === 'user') { 468 | if (placeToDelete.user_id !== loggedUser.id) { 469 | throwUnauthorizedError(next); 470 | } 471 | } 472 | 473 | let splitUrl = data.photo ? data.photo.split('/') : '' 474 | let imageName = typeof splitUrl !== 'string' ? splitUrl[splitUrl.length - 1] : '' 475 | return deleteImage(imageName) 476 | }) 477 | .then(function(data) { 478 | return placeToDelete.destroy() 479 | }) 480 | .then(function (data) { 481 | response.json({ 482 | message: 'Deleted successfully' 483 | }) 484 | }) 485 | .catch(next) 486 | } 487 | 488 | module.exports = function (LocalModel) { 489 | return new LocalController(LocalModel) 490 | } 491 | --------------------------------------------------------------------------------