├── .autorelease.yml ├── .bmp.yml ├── .editorconfig ├── .gitignore ├── .releaseignore ├── LICENSE ├── README.md ├── circle.yml ├── default-values ├── build-info.json ├── empty-domain-dir │ └── .gitkeep ├── model-config.json ├── models │ ├── application.json │ ├── installation.json │ ├── notification.json │ └── push.json └── non-model-configs │ ├── datasources.json │ ├── middleware.json │ ├── push-credentials.json │ └── server.json ├── gulpfile.coffee ├── loopback ├── common │ └── README.md └── server │ ├── boot │ ├── admin.js │ ├── authentication.js │ ├── custom-roles.js │ ├── explorer.js │ ├── notification.js │ ├── participant.js │ ├── rest-api.js │ └── root.js │ ├── custom-roles │ └── .gitkeep │ └── server.js ├── package.json ├── spec ├── custom-roles │ └── abc.js ├── global.js ├── lib │ ├── acl-generator.coffee │ ├── build-info-generator.coffee │ ├── config-json-generator.coffee │ ├── custom-configs.coffee │ ├── loopback-boot-generator.coffee │ ├── loopback-info.coffee │ ├── loopback-server.coffee │ ├── model-config-generator.coffee │ ├── model-definition.coffee │ ├── models-generator.coffee │ ├── music-live-configs │ │ ├── common │ │ │ ├── plain.coffee │ │ │ └── server.json │ │ ├── development │ │ │ └── server.coffee │ │ ├── local │ │ │ └── plain.coffee │ │ └── model-definitions.coffee │ └── sample-configs │ │ ├── common │ │ └── plain.coffee │ │ └── local │ │ └── plain.coffee └── main.coffee └── src ├── lib ├── acl-conditions.coffee ├── acl-generator.coffee ├── build-info-generator.coffee ├── config-json-generator.coffee ├── custom-configs.coffee ├── loopback-boot-generator.coffee ├── loopback-info.coffee ├── loopback-server.coffee ├── model-config-generator.coffee ├── model-definition.coffee └── models-generator.coffee ├── main.coffee └── server ├── admin-token-manager.coffee ├── boot └── notification.coffee └── participant-token-setter.coffee /.autorelease.yml: -------------------------------------------------------------------------------- 1 | hooks: 2 | release: 3 | pre: 4 | - gulp coffee 5 | gh_pages: 6 | pre: 7 | - gulp yuidoc 8 | config: 9 | git_user_name: CircleCI 10 | git_user_email: circleci@cureapp.jp 11 | version_prefix: v 12 | create_branch: false 13 | create_gh_pages: true 14 | gh_pages_dir: doc 15 | npm_update_depth: 3 16 | npm_shrinkwrap: false 17 | circle: 18 | machine: 19 | environment: 20 | NODE_ENV: 21 | timezone: Asia/Tokyo 22 | node: 23 | version: 6.9.1 24 | notify: 25 | webhooks: 26 | - url: "https://script.google.com/macros/s/AKfycbwJJOSKRGQYeyWDqiOCrmUOoBZ2xHsXFWFGZXLyhKcKEFPcWg8M/exec" 27 | -------------------------------------------------------------------------------- /.bmp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.6.1 3 | commit: Bump to version v%.%.% 4 | files: 5 | package.json: '"version": "%.%.%",' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*.js] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.coffee] 16 | indent_style = space 17 | indent_size = 4 18 | end_of_line = lf 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | coverage 18 | node_modules 19 | npm-debug.log 20 | loopback/common/models 21 | loopback/server/*.json 22 | loopback/server/custom-roles/*.js 23 | tmp 24 | dist 25 | doc 26 | -------------------------------------------------------------------------------- /.releaseignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .editorconfig 3 | spec 4 | .releaseignore 5 | .gitignore 6 | .bmp.yml 7 | circle.yml 8 | npm-debug.log 9 | 10 | README.litcoffee 11 | Gruntfile.coffee 12 | src 13 | doc 14 | 15 | loopback/common/models 16 | loopback/server/*.json 17 | tmp 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 CureApp.Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-with-admin 2 | 3 | Run loopback server easier. 4 | 5 | # features 6 | - passing model definitions via arguments (no need to generate JSON files) 7 | - switching environment easier 8 | - admin role, which can access to all endpoints 9 | - easier ACL settings 10 | - easier custom role settings 11 | - easier push notification settings 12 | 13 | 14 | # install 15 | 16 | ```bash 17 | npm install loopback-with-admin 18 | ``` 19 | 20 | # usage 21 | ## simplest run 22 | 23 | ```javascript 24 | // model definitions 25 | // see "models" section for more detail 26 | const models = { 27 | user: { 28 | base: 'User' 29 | } 30 | }; 31 | 32 | require('loopback-with-admin').run(models).then(lbInfo => { 33 | // see "LoopbackInfo" section for more detail 34 | console.log(lbInfo.getURL()) // loopback api root 35 | console.log(lbInfo.getAdminTokens()) // access tokens of admin 36 | }) 37 | ``` 38 | 39 | Or more strictly, pass `models` like 40 | 41 | ```javascript 42 | require('loopback-with-admin').run({models: models}) 43 | ``` 44 | 45 | 46 | ## run with config dir 47 | 48 | before running, you can prepare a directory which contains custom config information. 49 | 50 | ```text 51 | (config-dir) # any name is acceptable 52 | |-- common 53 | | |-- server.coffee 54 | |-- development 55 | | `-- datasources.coffee 56 | `-- production 57 | `-- datasources.coffee 58 | ``` 59 | 60 | ```javascript 61 | const lbWithAdmin = require('loopback-with-admin') 62 | const configDir = '/path/to/config-dir' 63 | 64 | lbWithAdmin.run({models: models}, configDir).then(lbInfo => { 65 | // loopback started with the config 66 | }) 67 | 68 | 69 | ``` 70 | See "configs" section for more details. 71 | 72 | 73 | ## run with config object 74 | 75 | ```javascript 76 | 77 | const lbWithAdmin = require('loopback-with-admin') 78 | const config = {server: {port: 3001}} 79 | lbWithAdmin.run({models: models}, config) 80 | ``` 81 | 82 | ## switching environment 83 | 84 | ```javascript 85 | 86 | const configDir = '/path/to/config-dir' 87 | 88 | require('loopback-with-admin').run({models: models}, configDir, {env: 'production'}) 89 | ``` 90 | env is set following the rules. 91 | 92 | - uses the passed value if exists 93 | - uses NODE_ENV if exists 94 | - default value is 'development' 95 | 96 | 97 | When your config dir is 98 | 99 | ```text 100 | (config-dir) # any name is acceptable 101 | |-- common 102 | | |-- server.coffee 103 | |-- development 104 | | `-- datasources.coffee 105 | |-- local 106 | | `-- datasources.coffee 107 | |-- production 108 | | `-- datasources.coffee 109 | ``` 110 | 111 | 112 | and launching script like 113 | 114 | ```bash 115 | $ NODE_ENV=local node app.js 116 | ``` 117 | then, loopback-with-admin selects configs in "local" directory. 118 | 119 | 120 | 121 | # model definitions 122 | 123 | ```javascript 124 | const models = { 125 | player: { // model name 126 | base: 'User', // following loopback model definition 127 | aclType: 'admin' // only 'aclType' is the specific property for loopback-with-admin 128 | }, 129 | 130 | instrument: { // another model 131 | aclType: 'owner-read' 132 | } 133 | } 134 | 135 | require('loopback-with-admin').run({models: models}) 136 | ``` 137 | 138 | - Models should be the same format as [loopback model definition](http://docs.strongloop.com/display/public/LB/Customizing+models) except `aclType` value. 139 | - `name` is automatically set from definition information. 140 | - `plural` is set to **the same value as the name** unless you set manually. 141 | 142 | 143 | ## aclType for easier ACL settings 144 | `aclType` is prepared for defining complicated acls easier. 145 | loopback-with-admin generates acls from aclType with the following rules. 146 | 147 | aclType | meaning 148 | ----------------------|----------------------------------------------------- 149 | admin | only admin can CRUD the model (_default_) 150 | owner | the owner of the model can CRUD 151 | public-read | everyone can READ the model and admin can CRUD 152 | member-read | authenticated users can READ the model and admin can CRUD 153 | public-read-by-owner | CRUD by the owner, and read by everyone 154 | member-read-by-owner | CRUD by the owner, and read by authenticated users 155 | none | everyone can CRUD the model 156 | 157 | ### more detailed settings 158 | 159 | ```javascript 160 | const models = { 161 | player: { 162 | base: 'User', 163 | aclType: { 164 | owner: 'rwx', 165 | member: 'r' 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | aclType can be an object, whose key contains the following roles. 172 | 173 | - owner: `$owner` role in LoopBack 174 | - member: `$authenticated` role in LoopBack 175 | - public: `$everyone` role in LoopBack 176 | - participant: participant token (see `participant role` section) 177 | - [custom roles] : see `custom roles` section. 178 | 179 | The values of the keys are `rwx`, which is the same as Unix permission. 180 | `x` here means `EXECUTE` accessType in LoopBack. 181 | 182 | 183 | See loopback roles for instructions. 184 | https://docs.strongloop.com/display/public/LB/Defining+and+using+roles 185 | 186 | ### custom roles 187 | You can define custom roles like the following code. 188 | 189 | ```javascript 190 | const customRoles = { 191 | 'doctor': '/path/to/doctor-role.js', 192 | 'patient': '/path/to/patient-role.js' 193 | } 194 | 195 | require('loopback-with-admin').run({models: models, customRoles: customRoles}) 196 | ``` 197 | 198 | #### role-defining JS file 199 | 200 | In the file, you must export a function, which will be passed to the 2nd argument of `Role.registerResolver` in LoopBack. 201 | 202 | See how to define custom roles in LoopBack. 203 | https://docs.strongloop.com/display/public/LB/Defining+and+using+roles 204 | 205 | Example: 206 | 207 | ```javascript 208 | 209 | module.exports = function(role, context, cb) { 210 | var app = this // `app` can be acquired via `this` 211 | 212 | function reject(err) { 213 | if (err) { return cb(err) } 214 | cb(null, false) 215 | } 216 | 217 | if (context.modelName !== 'patient') { return reject() } 218 | 219 | var userId = context.accessToken.userId 220 | if (!userId || userId === context.modelId) { 221 | return reject() 222 | } 223 | 224 | cb(null, true) // is in role 225 | ``` 226 | 227 | 228 | # admin role 229 | **`admin` role is the role with which every REST APIs are available**. 230 | The role and one user with it are automatically generated at boot phase. 231 | 232 | ## admin access tokens 233 | To be `admin`, you need to know its access tokens. The following code can get those. 234 | 235 | ```javascript 236 | require('loopback-with-admin').run(models, config).then(lbInfo => { 237 | let tokens = lbInfo.getAdminTokens() 238 | console.log(tokens) // access tokens (String[]) of admin. 239 | }) 240 | ``` 241 | 242 | ## set `fetch` function to set tokens 243 | By default, the token is fixed and it's `loopback-with-admin-token`. 244 | **You must change the value by passing `fetch` function**. 245 | 246 | ```javascript 247 | const admin = { 248 | fetch: function() { 249 | return ['your-secret-token1', 'your-secret-token2'] 250 | } 251 | } 252 | 253 | require('loopback-with-admin').run(models, config, { admin: admin }) 254 | ``` 255 | 256 | ## change tokens periodically 257 | 258 | ```javascript 259 | const admin = { 260 | fetch: function() { 261 | return generateSecretValuesByRandom().then(value => [ value ]) // fetch function allows Promise to return 262 | }, 263 | intervalHours: 24 // change the value every day (by default, it's 12 hours) 264 | } 265 | 266 | require('loopback-with-admin').run(models, config, { admin: admin }) 267 | ``` 268 | 269 | 270 | ## admin user information 271 | property | value 272 | -------------|-------------------------------- 273 | id | loopback-with-admin-user-id 274 | email | loopback-with-admin@example.com 275 | password | admin-user-password 276 | 277 | In fact, these value makes no sense as `admin` can **never be accessed via REST APIs**. No one can login with the account information. 278 | 279 | 280 | 281 | # configs 282 | 283 | Four types of configs are available. 284 | 285 | - datasources 286 | - middleware 287 | - server 288 | - push-credentials 289 | 290 | See JSON files in [default-values/non-model-configs directory](https://github.com/CureApp/loopback-with-admin/tree/master/default-values/non-model-configs). 291 | 292 | You can set the same properties as these JSONs. 293 | 294 | 295 | ## datasources 296 | 297 | config key | meaning 298 | -------------|--------------------------------- 299 | memory | on memory datasource 300 | db | datasource for custom entities 301 | 302 | Each datasource name has its connectors. 303 | 304 | ### available loopback connectors 305 | 306 | Available datasources are 307 | - mongodb 308 | - memory 309 | - memory-idstr 310 | 311 | `memory-idstr` is the default connector, which stores data only in memory, 312 | and id type is string whereas id type of "memory" is number. 313 | See [loopback-connector-memory-idstr](https://github.com/CureApp/loopback-connector-memory-idstr). 314 | 315 | To use mongodb, add dependencies in package.json of your repository 316 | 317 | - loopback-connector-mongodb: "1.13.0" 318 | - mongodb: "2.0.35" 319 | 320 | 321 | ## server 322 | 323 | config key | meaning | default 324 | -------------|---------------|---------------- 325 | restApiRoot | REST api root | /api 326 | port | port number | 3000 327 | 328 | 329 | ## push-credentials 330 | 331 | config key | meaning 332 | -----------------|------------------------------------------- 333 | gcmServerApiKey | api key for Google Cloud Messaging (GCM) 334 | apnsCertData | certificate pem contents for APNs 335 | apnsKeyData | key pem contents for APNs 336 | 337 | # LoopbackInfo 338 | 339 | `require('loopback-with-admin').run()` returns promise of `LoopbackInfo`. 340 | 341 | It contains the information of the launched loopback. 342 | 343 | - getURL() 344 | - getAdminTokens() 345 | - config 346 | - models 347 | 348 | ## getURL() 349 | Returns hosting URL. 350 | 351 | ```javascript 352 | const config = { 353 | server: { 354 | port: 4156, 355 | restApiRoot: 'awesome-endpoint' 356 | } 357 | } 358 | require('loopback-with-admin').run(models, config).then(lbInfo => { 359 | lbInfo.getURL() // localhost:4156/awesome-endpoint 360 | }) 361 | ``` 362 | 363 | 364 | ## getAdminTokens() 365 | Returns Array of access tokens (string). 366 | 367 | ```javascript 368 | const admin = { 369 | fetch: function() { 370 | return ['your-secret-token1', 'your-secret-token2'] 371 | } 372 | } 373 | 374 | require('loopback-with-admin').run(models, config, { admin: admin }).then(lbInfo => { 375 | console.log(lbInfo.getAdminTokens()) // ['your-secret-token1', 'your-secret-token2'] 376 | }) 377 | ``` 378 | 379 | 380 | ## getEnv() 381 | Returns environment name where loopback launched. 382 | 383 | 384 | ## config 385 | Contains all config values used to build loopback. 386 | 387 | - datasources 388 | - middleware 389 | - server 390 | - push-credentials 391 | 392 | See configs section above. 393 | 394 | ## models 395 | Contains model definitions used to build loopback 396 | 397 | See models section above. 398 | 399 | # participant role (since v2.4) 400 | The role for anonymous but limited access. 401 | Programs which know a specific static access token can become the role. 402 | The token is set by options param in `run()` method. 403 | 404 | ``` 405 | const participantToken = 'AbCdE' 406 | require('loopback-with-admin').run(models, config, { participantToken }) 407 | ``` 408 | 409 | Then, all accesses with the accessToken `AbCdE` will be `participant` role. 410 | 411 | # push notification settings 412 | (coming soon) 413 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - gh-pages 5 | - /release.*/ 6 | machine: 7 | environment: 8 | PATH: './node_modules/.bin:$PATH' 9 | NODE_ENV: null 10 | pre: 11 | - git config --global user.name "CircleCI" 12 | - git config --global user.email "circleci@cureapp.jp" 13 | timezone: Asia/Tokyo 14 | node: 15 | version: 6.9.1 16 | dependencies: 17 | post: 18 | - nca run nca update-modules --depth 3 19 | deployment: 20 | create_release_branch: 21 | branch: 22 | - master 23 | commands: 24 | - nca run gulp coffee 25 | - nca release --prefix v 26 | - nca run gulp yuidoc 27 | - nca run nca gh-pages --dir doc 28 | notify: 29 | webhooks: 30 | - url: 'https://script.google.com/macros/s/AKfycbwJJOSKRGQYeyWDqiOCrmUOoBZ2xHsXFWFGZXLyhKcKEFPcWg8M/exec' 31 | -------------------------------------------------------------------------------- /default-values/build-info.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /default-values/empty-domain-dir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CureApp/loopback-with-admin/4abffbb00787c94ed64211def0e153bf6febf9bb/default-values/empty-domain-dir/.gitkeep -------------------------------------------------------------------------------- /default-values/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ] 9 | }, 10 | "User": { 11 | "dataSource": "memory" 12 | }, 13 | "AccessToken": { 14 | "dataSource": "db", 15 | "public": true 16 | }, 17 | "ACL": { 18 | "dataSource": "memory", 19 | "public": false 20 | }, 21 | "RoleMapping": { 22 | "dataSource": "memory", 23 | "public": false 24 | }, 25 | "Role": { 26 | "dataSource": "memory", 27 | "public": false 28 | }, 29 | "application": { 30 | "public": false, 31 | "dataSource": "memory" 32 | }, 33 | "installation": { 34 | "public": true, 35 | "dataSource": "db" 36 | }, 37 | "notification": { 38 | "public": false, 39 | "dataSource": "memory" 40 | }, 41 | "push": { 42 | "public": true, 43 | "dataSource": "push" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /default-values/models/application.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "Application", 3 | "name": "application" 4 | } -------------------------------------------------------------------------------- /default-values/models/installation.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "Installation", 3 | "name": "installation", 4 | "plural": "installation", 5 | "acls": [ 6 | { 7 | "accessType": "*", 8 | "principalType": "ROLE", 9 | "principalId": "$everyone", 10 | "permission": "DENY" 11 | }, 12 | { 13 | "accessType": "*", 14 | "principalType": "ROLE", 15 | "principalId": "admin", 16 | "permission": "ALLOW" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /default-values/models/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "Notification", 3 | "name": "notification" 4 | } -------------------------------------------------------------------------------- /default-values/models/push.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "Model", 3 | "name": "push", 4 | "plural": "push", 5 | "properties": {}, 6 | "validations": [], 7 | "relations": {}, 8 | "methods": [], 9 | "acls": [ 10 | { 11 | "accessType": "*", 12 | "principalType": "ROLE", 13 | "principalId": "$everyone", 14 | "permission": "DENY" 15 | }, 16 | { 17 | "accessType": "*", 18 | "principalType": "ROLE", 19 | "principalId": "admin", 20 | "permission": "ALLOW" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /default-values/non-model-configs/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "memory": { 3 | "name": "memory", 4 | "connector": "memory-idstr" 5 | }, 6 | "db": { 7 | "name": "db", 8 | "connector": "memory-idstr" 9 | }, 10 | "push": { 11 | "name": "push", 12 | "connector": "loopback-component-push", 13 | "installation": "installation", 14 | "notification": "notification", 15 | "application": "application" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /default-values/non-model-configs/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {} 7 | }, 8 | "session": { 9 | }, 10 | "auth": { 11 | }, 12 | "parse": { 13 | }, 14 | "routes": { 15 | }, 16 | "files": { 17 | }, 18 | "final": { 19 | "loopback#urlNotFound": {} 20 | }, 21 | "final:after": { 22 | "errorhandler": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /default-values/non-model-configs/push-credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "gcmServerApiKey" : "dummy", 3 | "apnsCertData" : "dummy", 4 | "apnsKeyData" : "dummy" 5 | } 6 | -------------------------------------------------------------------------------- /default-values/non-model-configs/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "localhost", 4 | "port": 3000, 5 | "legacyExplorer": false, 6 | "remoting": { 7 | "context": { 8 | "enableHttpContext": false 9 | }, 10 | "rest": { 11 | "normalizeHttpPath": false, 12 | "xml": false 13 | }, 14 | "json": { 15 | "strict": false, 16 | "limit": "100kb" 17 | }, 18 | "urlencoded": { 19 | "extended": true, 20 | "limit": "100kb" 21 | }, 22 | "cors": { 23 | "origin": true, 24 | "credentials": true 25 | }, 26 | "errorHandler": { 27 | "disableStackTrace": false 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | coffee = require 'gulp-coffee' 3 | yuidoc = require 'gulp-yuidoc' 4 | 5 | 6 | gulp.task 'coffee', -> 7 | 8 | gulp.src 'src/**/*.coffee' 9 | .pipe(coffee bare: true) 10 | .pipe(gulp.dest 'dist') 11 | 12 | 13 | gulp.task 'yuidoc', -> 14 | 15 | gulp.src 'src/**/*.coffee' 16 | .pipe(yuidoc({ 17 | syntaxtype: 'coffee' 18 | project: 19 | name: 'loopback-with-admin' 20 | })) 21 | .pipe(gulp.dest('doc')) 22 | .on('error', console.log) 23 | 24 | module.exports = gulp 25 | -------------------------------------------------------------------------------- /loopback/common/README.md: -------------------------------------------------------------------------------- 1 | # common 2 | 3 | common/models is gitignored 4 | -------------------------------------------------------------------------------- /loopback/server/boot/admin.js: -------------------------------------------------------------------------------- 1 | // http://docs.strongloop.com/display/public/LB/Creating+a+default+admin+user 2 | 3 | module.exports = function(app, done) { 4 | 5 | // lwaTokenManager: instance of AdminTokenManager (src/server/admin-token-manager.coffee) 6 | // defined in LoopbackServer (src/lib/loopback-server.coffee) 7 | if (app.lwaTokenManager) { 8 | app.lwaTokenManager.init(app.models).then(function() { 9 | done() 10 | }) 11 | .catch(function(err) { 12 | console.log(err) 13 | console.log(err.stack) 14 | process.exit() 15 | }) 16 | } 17 | else { 18 | done() 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /loopback/server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | module.exports = function enableAuthentication(server) { 2 | // enable authentication 3 | server.enableAuth(); 4 | }; 5 | -------------------------------------------------------------------------------- /loopback/server/boot/custom-roles.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs') 3 | var customRolesDir = __dirname + '/../custom-roles' 4 | 5 | module.exports = function(app) { 6 | 7 | var Role = app.models.Role; 8 | 9 | fs.readdirSync(customRolesDir).forEach(function(filename) { 10 | 11 | if (filename.slice(-3) !== '.js') return; 12 | 13 | var fn = require(customRolesDir + '/' + filename) 14 | 15 | if (typeof fn !== 'function') return; 16 | 17 | var name = filename.slice(0, -3) 18 | 19 | Role.registerResolver(name, fn.bind(app)) 20 | 21 | }) 22 | }; 23 | -------------------------------------------------------------------------------- /loopback/server/boot/explorer.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountLoopBackExplorer(server) { 2 | var explorer; 3 | try { 4 | explorer = require('loopback-explorer'); 5 | } catch(err) { 6 | // Print the message only when the app was started via `server.listen()`. 7 | // Do not print any message when the project is used as a component. 8 | server.once('started', function(baseUrl) { 9 | console.log( 10 | 'Run `npm install loopback-explorer` to enable the LoopBack explorer' 11 | ); 12 | }); 13 | return; 14 | } 15 | 16 | var restApiRoot = server.get('restApiRoot'); 17 | 18 | var explorerApp = explorer(server, { basePath: restApiRoot }); 19 | server.use('/explorer', explorerApp); 20 | server.once('started', function() { 21 | var baseUrl = server.get('url').replace(/\/$/, ''); 22 | // express 4.x (loopback 2.x) uses `mountpath` 23 | // express 3.x (loopback 1.x) uses `route` 24 | var explorerPath = explorerApp.mountpath || explorerApp.route; 25 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /loopback/server/boot/notification.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.1 2 | (function() { 3 | var registerApp; 4 | 5 | module.exports = function(app, cb) { 6 | var Installation; 7 | Installation = app.models.installation; 8 | Installation.observe('before save', function(ctx, next) { 9 | if (ctx.instance) { 10 | ctx.instance.appId = 'loopback-with-admin'; 11 | } 12 | return next(); 13 | }); 14 | return registerApp(app, cb); 15 | }; 16 | 17 | 18 | /** 19 | registers an application instance for push notification service 20 | 21 | @method registerApp 22 | */ 23 | 24 | registerApp = function(app, cb) { 25 | var Application, buildInfo, config; 26 | Application = app.models.application; 27 | config = require('../push-credentials'); 28 | buildInfo = require('../build-info'); 29 | Application.observe('before save', function(ctx, next) { 30 | ctx.instance.id = 'loopback-with-admin'; 31 | return next(); 32 | }); 33 | return Application.register('CureApp, Inc.', 'loopback-with-admin', { 34 | descriptions: '', 35 | pushSettings: { 36 | apns: { 37 | production: buildInfo.env === 'production', 38 | certData: config.apnsCertData, 39 | keyData: config.apnsKeyData, 40 | feeedbackOptions: { 41 | batchFeedback: true, 42 | interval: 300 43 | } 44 | }, 45 | gcm: { 46 | serverApiKey: config.gcmServerApiKey 47 | } 48 | } 49 | }, function(err, savedApp) { 50 | if (err) { 51 | console.log(err); 52 | } 53 | return cb(); 54 | }); 55 | }; 56 | 57 | }).call(this); 58 | -------------------------------------------------------------------------------- /loopback/server/boot/participant.js: -------------------------------------------------------------------------------- 1 | // http://docs.strongloop.com/display/public/LB/Creating+a+default+admin+user 2 | 3 | module.exports = function(app, done) { 4 | 5 | if (app.participantTokenSetter) { 6 | app.participantTokenSetter.set(app.models).then(function() { 7 | done() 8 | }) 9 | .catch(function(err) { 10 | console.log(err) 11 | console.log(err.stack) 12 | process.exit() 13 | }) 14 | } 15 | else { 16 | done() 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /loopback/server/boot/rest-api.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountRestApi(server) { 2 | var restApiRoot = server.get('restApiRoot'); 3 | server.use(restApiRoot, server.loopback.rest()); 4 | }; 5 | -------------------------------------------------------------------------------- /loopback/server/boot/root.js: -------------------------------------------------------------------------------- 1 | module.exports = function(server) { 2 | // Install a `/` route that returns server status 3 | var router = server.loopback.Router(); 4 | router.get('/', server.loopback.status()); 5 | server.use(router); 6 | }; 7 | -------------------------------------------------------------------------------- /loopback/server/custom-roles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CureApp/loopback-with-admin/4abffbb00787c94ed64211def0e153bf6febf9bb/loopback/server/custom-roles/.gitkeep -------------------------------------------------------------------------------- /loopback/server/server.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var boot = require('loopback-boot'); 3 | var helmet = require('helmet'); 4 | 5 | var app = module.exports = loopback(); 6 | 7 | app.use(helmet.ieNoOpen()) 8 | app.use(helmet.noSniff()) 9 | app.use(helmet.xssFilter()) 10 | app.use(helmet.hidePoweredBy()) 11 | 12 | app.start = function(callback) { 13 | boot(app, __dirname, function(err) { 14 | if (err) return callback(err) 15 | 16 | // start the web server 17 | app.listen(function(err) { 18 | 19 | if (err) return callback(err) 20 | 21 | app.emit('started'); 22 | console.log('Web server listening at: %s', app.get('url')); 23 | 24 | console.log('LOOPBACK_WITH_ADMIN_STARTED'); 25 | 26 | callback() 27 | }); 28 | }); 29 | }; 30 | 31 | // Bootstrap the application, configure models, datasources and middleware. 32 | // Sub-apps like REST API are mounted via boot scripts. 33 | 34 | // start the server if `$ node server.js` 35 | if (require.main === module) { 36 | app.start(); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-with-admin", 3 | "version": "2.6.1", 4 | "main": "dist/main.js", 5 | "scripts": { 6 | "test": "mocha -r spec/global.js spec/main.coffee spec/lib/*.coffee", 7 | "single": "mocha -r spec/global.js", 8 | "build": "gulp coffee" 9 | }, 10 | "author": "CureApp, Inc.", 11 | "license": "MIT", 12 | "directories": { 13 | "test": "spec" 14 | }, 15 | "dependencies": { 16 | "coffee-script": "^1.10.0", 17 | "compression": "^1.6.1", 18 | "debug": "^2.2.0", 19 | "errorhandler": "^1.4.3", 20 | "fs-extra": "^0.27.0", 21 | "helmet": "^3.9.0", 22 | "loopback": "^2.42.0", 23 | "loopback-boot": "^2.18.1", 24 | "loopback-component-push": "^1.5.3", 25 | "loopback-connector-memory-idstr": "^1.0.0", 26 | "loopback-datasource-juggler": "^2.46.0", 27 | "serve-favicon": "^2.0.1" 28 | }, 29 | "optionalDependencies": { 30 | "loopback-explorer": "^1.8.0" 31 | }, 32 | "devDependencies": { 33 | "espower-coffee": "^1.0.1", 34 | "gulp": "^3.9.1", 35 | "gulp-coffee": "^2.3.2", 36 | "gulp-plumber": "^1.1.0", 37 | "gulp-yuidoc": "^0.1.2", 38 | "mocha": "^2.4.5", 39 | "node-circleci-autorelease": "^2.1.8", 40 | "power-assert": "^1.3.1" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/CureApp/loopback-with-admin" 45 | }, 46 | "description": "launch loopback with admin, push-notifications" 47 | } 48 | -------------------------------------------------------------------------------- /spec/custom-roles/abc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(role, context, cb) { 3 | 4 | cb(null, context.modelName === 'abc') 5 | 6 | }; 7 | -------------------------------------------------------------------------------- /spec/global.js: -------------------------------------------------------------------------------- 1 | require('espower-coffee/guess'); 2 | assert = require('power-assert'); 3 | -------------------------------------------------------------------------------- /spec/lib/acl-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | AclGenerator = require '../../src/lib/acl-generator' 3 | 4 | describe 'AclGenerator', -> 5 | 6 | commonACL = new AclGenerator().commonACL().acl 7 | 8 | describe 'userACL', -> 9 | 10 | before -> 11 | @aclGenerator = new AclGenerator() 12 | @aclGenerator.userACL() 13 | 14 | it 'appends three ACs', -> 15 | assert @aclGenerator.acl.length is 3 16 | 17 | it 'appends AC denying logout by admin', -> 18 | ac = @aclGenerator.acl[0] 19 | assert ac.principalType is 'ROLE' 20 | assert ac.principalId is 'admin' 21 | assert ac.permission is 'DENY' 22 | assert ac.property is 'logout' 23 | 24 | it 'appends AC denying creation by everyone', -> 25 | ac = @aclGenerator.acl[1] 26 | assert ac.principalType is 'ROLE' 27 | assert ac.principalId is '$everyone' 28 | assert ac.permission is 'DENY' 29 | assert ac.property is 'create' 30 | 31 | it 'appends AC allowing creation by admin', -> 32 | ac = @aclGenerator.acl[2] 33 | assert ac.principalType is 'ROLE' 34 | assert ac.principalId is 'admin' 35 | assert ac.permission is 'ALLOW' 36 | assert ac.property is 'create' 37 | 38 | 39 | describe 'commonACL', -> 40 | 41 | describe 'with non-user model', -> 42 | 43 | before -> 44 | @aclGenerator = new AclGenerator() 45 | @aclGenerator.commonACL() 46 | 47 | it 'appends two ACs', -> 48 | assert @aclGenerator.acl.length is 2 49 | 50 | it 'appends AC denying everyone\'s access in the first place', -> 51 | ac = @aclGenerator.acl[0] 52 | 53 | assert ac.principalType is 'ROLE' 54 | assert ac.principalId is '$everyone' 55 | assert ac.permission is 'DENY' 56 | 57 | it 'appends AC allowing admin\'s access', -> 58 | ac = @aclGenerator.acl[1] 59 | 60 | assert ac.principalType is 'ROLE' 61 | assert ac.principalId is 'admin' 62 | assert ac.permission is 'ALLOW' 63 | 64 | 65 | describe 'with user model', -> 66 | 67 | it 'appends five ACs', -> 68 | aclGenerator = new AclGenerator(null, true) 69 | aclGenerator.commonACL() 70 | assert aclGenerator.acl.length is 5 71 | 72 | 73 | it 'userACL() is called after basic ACL are appended', (done) -> 74 | aclGenerator = new AclGenerator(null, true) 75 | 76 | aclGenerator.userACL = -> 77 | assert aclGenerator.acl.length is 2 78 | done() 79 | 80 | aclGenerator.commonACL() 81 | 82 | 83 | describe 'adminUserACL', -> 84 | before -> 85 | @aclGenerator = new AclGenerator() 86 | @aclGenerator.adminUserACL() 87 | 88 | it 'appends two ACs', -> 89 | assert @aclGenerator.acl.length is 2 90 | 91 | it 'appends AC denying login by everyone', -> 92 | ac = @aclGenerator.acl[0] 93 | assert ac.principalType is 'ROLE' 94 | assert ac.principalId is '$everyone' 95 | assert ac.permission is 'DENY' 96 | assert ac.property is 'login' 97 | 98 | it 'appends AC denying creation by everyone', -> 99 | ac = @aclGenerator.acl[1] 100 | assert ac.principalType is 'ROLE' 101 | assert ac.principalId is 'admin' 102 | assert ac.permission is 'ALLOW' 103 | assert ac.property is 'login' 104 | 105 | 106 | 107 | 108 | describe 'generate', -> 109 | 110 | it 'appends no ACL when aclType is "none"', -> 111 | 112 | aclGenerator = new AclGenerator('none', false) 113 | assert.deepEqual aclGenerator.acl, [] 114 | 115 | it 'appends no ACL when aclType is "none" even if it is user model', -> 116 | 117 | aclGenerator = new AclGenerator('none', true) 118 | assert.deepEqual aclGenerator.acl, [] 119 | 120 | 121 | describe 'when aclType is "admin",', -> 122 | 123 | it 'appends two ACs, the same as commonACL() with nonUser model', -> 124 | 125 | aclType = 'admin' 126 | isUser = false 127 | acl = new AclGenerator(aclType, isUser).generate() 128 | 129 | assert acl.length is 2 130 | assert.deepEqual acl, commonACL 131 | 132 | 133 | it 'appends seven ACs with user model', -> 134 | 135 | aclType = 'admin' 136 | isUser = true 137 | acl = new AclGenerator(aclType, isUser).generate() 138 | 139 | assert acl.length is 7 140 | 141 | 142 | describe 'when aclType is "owner",', -> 143 | 144 | 145 | it 'appends five ACs, the first two are the same as commonACL() with nonUser model', -> 146 | aclType = 'owner' 147 | isUser = false 148 | acl = new AclGenerator(aclType, isUser).generate() 149 | 150 | assert acl.length is 5 151 | assert.deepEqual acl.slice(0,2), commonACL 152 | 153 | 154 | it 'appends AC allowing read, write, by admin for owner with nonUser', -> 155 | 156 | aclType = 'owner' 157 | isUser = false 158 | acl = new AclGenerator(aclType, isUser).generate() 159 | 160 | accessTypes = ['READ', 'WRITE', 'EXECUTE'] 161 | 162 | for ac in acl.slice(2) 163 | assert ac.principalType is 'ROLE' 164 | assert ac.principalId is '$owner' 165 | assert ac.permission is 'ALLOW' 166 | assert ac.accessType in accessTypes 167 | 168 | it 'appends eight ACs with user model', -> 169 | aclType = 'owner' 170 | isUser = true 171 | acl = new AclGenerator(aclType, isUser).generate() 172 | 173 | assert acl.length is 8 174 | 175 | 176 | describe 'when aclType is "public-read",', -> 177 | 178 | it 'appends three ACs, the first two are the same as commonACL() with nonUser model', -> 179 | 180 | aclType = 'public-read' 181 | isUser = false 182 | acl = new AclGenerator(aclType, isUser).generate() 183 | 184 | assert acl.length is 3 185 | assert.deepEqual acl.slice(0,2), commonACL 186 | 187 | it 'appends AC allowing everyone to READ with nonUser model', -> 188 | 189 | aclType = 'public-read' 190 | isUser = false 191 | acl = new AclGenerator(aclType, isUser).generate() 192 | 193 | for ac in acl.slice(2) 194 | assert ac.principalType is 'ROLE' 195 | assert ac.principalId is '$everyone' 196 | assert ac.permission is 'ALLOW' 197 | assert ac.accessType is 'READ' 198 | 199 | it 'appends ACs with user model', -> 200 | 201 | aclType = 'public-read' 202 | isUser = true 203 | acl = new AclGenerator(aclType, isUser).generate() 204 | 205 | assert acl.length is 6 206 | 207 | 208 | describe 'when aclType is "member-read",', -> 209 | 210 | it 'appends three ACs, the first two are the same as commonACL() with nonUser model', -> 211 | 212 | aclType = 'member-read' 213 | isUser = false 214 | acl = new AclGenerator(aclType, isUser).generate() 215 | 216 | assert acl.length is 3 217 | assert.deepEqual acl.slice(0,2), commonACL 218 | 219 | it 'appends AC allowing authenticated users to READ with nonUser model', -> 220 | 221 | aclType = 'member-read' 222 | isUser = false 223 | acl = new AclGenerator(aclType, isUser).generate() 224 | 225 | for ac in acl.slice(2) 226 | assert ac.principalType is 'ROLE' 227 | assert ac.principalId is '$authenticated' 228 | assert ac.permission is 'ALLOW' 229 | assert ac.accessType is 'READ' 230 | 231 | it 'appends ACs with user model', -> 232 | 233 | aclType = 'member-read' 234 | isUser = true 235 | acl = new AclGenerator(aclType, isUser).generate() 236 | 237 | assert acl.length is 6 238 | 239 | 240 | 241 | describe 'when aclType is "member-read-by-owner",', -> 242 | 243 | it 'appends five ACs, the first two are the same as commonACL() with nonUser model', -> 244 | 245 | aclType = 'member-read-by-owner' 246 | isUser = false 247 | acl = new AclGenerator(aclType, isUser).generate() 248 | 249 | assert acl.length is 5 250 | assert.deepEqual acl.slice(0,2), commonACL 251 | 252 | it 'appends AC allowing read for member, write and execute for owner', -> 253 | 254 | aclType = 'member-read-by-owner' 255 | isUser = false 256 | acl = new AclGenerator(aclType, isUser).generate() 257 | 258 | acl2 = acl[2] 259 | assert acl2.principalType is 'ROLE' 260 | assert acl2.principalId is '$authenticated' 261 | assert acl2.permission is 'ALLOW' 262 | 263 | 264 | accessTypes = ['WRITE', 'EXECUTE'] 265 | 266 | for ac in acl.slice(3, 4) 267 | assert ac.principalType is 'ROLE' 268 | assert ac.principalId is '$owner' 269 | assert ac.permission is 'ALLOW' 270 | assert ac.accessType in accessTypes 271 | 272 | 273 | it 'appends ACs with user model', -> 274 | 275 | aclType = 'member-read-by-owner' 276 | isUser = true 277 | acl = new AclGenerator(aclType, isUser).generate() 278 | 279 | assert acl.length is 8 280 | 281 | 282 | describe 'when aclType is "public-read-by-owner",', -> 283 | 284 | 285 | it 'appends five ACs, the first two are the same as commonACL() with nonUser model', -> 286 | 287 | aclType = 'public-read-by-owner' 288 | isUser = false 289 | acl = new AclGenerator(aclType, isUser).generate() 290 | 291 | assert acl.length is 5 292 | assert.deepEqual acl.slice(0, 2), commonACL 293 | 294 | 295 | it 'appends AC allowing read, write, by admin for owner with nonUser', -> 296 | 297 | aclType = 'public-read-by-owner' 298 | isUser = false 299 | acl = new AclGenerator(aclType, isUser).generate() 300 | 301 | accessTypes = ['WRITE', 'EXECUTE'] 302 | 303 | acl2 = acl[2] 304 | assert acl2.principalType is 'ROLE' 305 | assert acl2.principalId is '$everyone' 306 | assert acl2.permission is 'ALLOW' 307 | 308 | 309 | for ac in acl.slice(3, 4) 310 | assert ac.principalType is 'ROLE' 311 | assert ac.principalId is '$owner' 312 | assert ac.permission is 'ALLOW' 313 | assert ac.accessType in accessTypes 314 | 315 | 316 | it 'appends ACs with user model', -> 317 | 318 | aclType = 'public-read-by-owner' 319 | isUser = true 320 | acl = new AclGenerator(aclType, isUser).generate() 321 | 322 | assert acl.length is 8 323 | 324 | describe 'when aclType is "custom" and give permission to write', -> 325 | 326 | it 'appends AC allowing create and updateAttributes', -> 327 | 328 | aclType = 329 | 'my-custom': 'rw' 330 | isUser = true 331 | acl = new AclGenerator(aclType, isUser).generate() 332 | 333 | assert acl[6].principalType is 'ROLE' 334 | assert acl[6].principalId is 'my-custom' 335 | assert acl[6].permission is 'ALLOW' 336 | assert acl[6].accessType is 'WRITE' 337 | assert acl[6].property is 'create' 338 | 339 | assert acl[7].principalType is 'ROLE' 340 | assert acl[7].principalId is 'my-custom' 341 | assert acl[7].permission is 'ALLOW' 342 | assert acl[7].accessType is 'WRITE' 343 | assert acl[7].property is 'updateAttributes' 344 | 345 | 346 | -------------------------------------------------------------------------------- /spec/lib/build-info-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | fs = require 'fs-extra' 5 | 6 | BuildInfoGenerator = require '../../src/lib/build-info-generator' 7 | 8 | 9 | describe 'BuildInfoGenerator', -> 10 | 11 | 12 | describe 'getMergedConfig', -> 13 | 14 | before -> 15 | 16 | modelDefinitions = player: 'base': 'User' 17 | configObj = server: port: 3001 18 | env = 'production' 19 | 20 | @info = new BuildInfoGenerator(modelDefinitions, configObj, env).getMergedConfig() 21 | 22 | it 'contains modelDefinitions', -> 23 | assert @info.hasOwnProperty 'modelDefinitions' 24 | assert @info.modelDefinitions.player.base is 'User' 25 | 26 | it 'contains custom configs', -> 27 | assert @info.hasOwnProperty 'customConfigs' 28 | 29 | it 'contains buildAt', -> 30 | assert @info.hasOwnProperty 'buildAt' 31 | buildAt = @info.buildAt 32 | time = new Date(buildAt) 33 | assert new Date() - time < 1000 34 | 35 | it 'contains env info', -> 36 | assert @info.env is 'production' 37 | 38 | 39 | describe 'getDestinationPathByName', -> 40 | 41 | it 'returns build-info.json', -> 42 | generator = new BuildInfoGenerator() 43 | path = generator.getDestinationPathByName('build-info') 44 | assert path is normalize __dirname + '/../../loopback/server/build-info.json' 45 | 46 | 47 | describe 'loadDefaultConfig', -> 48 | 49 | it 'loads build-info', -> 50 | config = new BuildInfoGenerator().loadDefaultConfig('build-info') 51 | assert typeof config is 'object' 52 | 53 | describe 'generate', -> 54 | 55 | before -> 56 | @generator = new BuildInfoGenerator({}, {}, 'development') 57 | @generator.destinationPath = __dirname + '/d' 58 | fs.mkdirsSync __dirname + '/d' 59 | 60 | after -> 61 | fs.removeSync __dirname + '/d' 62 | 63 | 64 | it 'returns build info', -> 65 | 66 | generated = @generator.generate() 67 | assert generated.env is 'development' 68 | assert generated.hasOwnProperty 'buildAt' 69 | assert generated.hasOwnProperty 'modelDefinitions' 70 | assert generated.hasOwnProperty 'customConfigs' 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/lib/config-json-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | fs = require 'fs' 5 | 6 | ConfigJSONGenerator = require '../../src/lib/config-json-generator' 7 | 8 | 9 | describe 'ConfigJSONGenerator', -> 10 | 11 | 12 | describe 'getDestinationPathByName', -> 13 | 14 | before -> 15 | @generator = new ConfigJSONGenerator() 16 | 17 | it 'returns #{configName}.json', -> 18 | path = @generator.getDestinationPathByName('datasources') 19 | assert path is normalize __dirname + '/../../loopback/server/datasources.json' 20 | 21 | 22 | it 'returns config.json when "server" is given', -> 23 | path = @generator.getDestinationPathByName('server') 24 | assert path is normalize __dirname + '/../../loopback/server/config.json' 25 | 26 | 27 | describe 'merge', -> 28 | 29 | before -> 30 | @generator = new ConfigJSONGenerator() 31 | 32 | it 'merges two object, 1st argument is dominant', -> 33 | dominant = 34 | common: 'dominant' 35 | onlyDominant: true 36 | 37 | base = 38 | common: 'base' 39 | onlyBase: true 40 | 41 | merged = @generator.merge(dominant, base) 42 | assert merged.common is 'dominant' 43 | assert merged.onlyDominant is true 44 | assert merged.onlyBase is true 45 | 46 | 47 | it 'merges sub objects, 1st argument is dominant', -> 48 | dominant = 49 | common: 'dominant' 50 | sub: 51 | common: 'dominant' 52 | onlySubDominant: true 53 | subsub: 54 | common: 'dominant' 55 | onlySubSubDominant: true 56 | 57 | base = 58 | common: 'base' 59 | sub: 60 | common: 'base' 61 | onlySubBase: true 62 | subsub: 63 | common: 'base' 64 | onlySubSubBase: true 65 | 66 | merged = @generator.merge(dominant, base) 67 | assert merged.common is 'dominant' 68 | assert merged.hasOwnProperty 'sub' 69 | 70 | assert merged.sub.common is 'dominant' 71 | assert merged.sub.onlySubDominant is true 72 | assert merged.sub.onlySubBase is true 73 | assert merged.sub.hasOwnProperty 'subsub' 74 | 75 | assert merged.sub.subsub.common is 'dominant' 76 | assert merged.sub.subsub.onlySubSubDominant is true 77 | assert merged.sub.subsub.onlySubSubBase is true 78 | 79 | 80 | describe 'loadDefaultConfig', -> 81 | 82 | it 'loads datasources', -> 83 | config = new ConfigJSONGenerator().loadDefaultConfig('datasources') 84 | assert typeof config is 'object' 85 | assert config.hasOwnProperty 'memory' 86 | assert config.hasOwnProperty 'db' 87 | 88 | it 'loads middleware', -> 89 | config = new ConfigJSONGenerator().loadDefaultConfig('middleware') 90 | assert typeof config is 'object' 91 | 92 | it 'does not load model-config', -> 93 | config = new ConfigJSONGenerator().loadDefaultConfig('model-config') 94 | assert not config? 95 | 96 | it 'loads server', -> 97 | config = new ConfigJSONGenerator().loadDefaultConfig('server') 98 | assert typeof config is 'object' 99 | assert config.port is 3000 100 | 101 | it 'loads push-credentials', -> 102 | config = new ConfigJSONGenerator().loadDefaultConfig('push-credentials') 103 | assert typeof config is 'object' 104 | assert config.hasOwnProperty 'gcmServerApiKey' 105 | assert config.hasOwnProperty 'apnsCertData' 106 | assert config.hasOwnProperty 'apnsKeyData' 107 | 108 | 109 | describe 'getMergedConfig', -> 110 | 111 | it 'merges custom and default configs', -> 112 | 113 | customConfigObj = 114 | datasources: 115 | memory: 116 | name: "memory" 117 | connector: "memory-idstr123" 118 | memory2: 119 | name: "memory" 120 | connector: "memory" 121 | 122 | models: {} 123 | xxx: 'yyy' 124 | 125 | generator = new ConfigJSONGenerator(customConfigObj) 126 | 127 | merged = generator.getMergedConfig('datasources') 128 | 129 | assert merged.memory.name is 'memory' 130 | assert merged.memory.connector is 'memory-idstr123' 131 | assert merged.db.name is 'db' # from default config 132 | assert merged.memory2.connector is 'memory' 133 | 134 | 135 | describe 'generate', -> 136 | 137 | before -> 138 | @tmpdir = __dirname + '/tmp' 139 | try 140 | fs.mkdirSync @tmpdir 141 | catch e 142 | 143 | @generator = new ConfigJSONGenerator(__dirname + '/sample-configs') 144 | @generator.destinationPath = @tmpdir 145 | @generatedContents = @generator.generate() 146 | 147 | after -> 148 | for fname in fs.readdirSync @tmpdir 149 | fs.unlinkSync @tmpdir + '/' + fname 150 | fs.rmdirSync @tmpdir 151 | 152 | it 'generates four json files', -> 153 | 154 | assert fs.readdirSync(@tmpdir).length is 4 155 | 156 | it 'generates config.json', -> 157 | 158 | assert 'config.json' in fs.readdirSync @tmpdir 159 | 160 | it 'returns generated contents', -> 161 | 162 | assert Object.keys @generatedContents.length is 5 163 | 164 | 165 | describe 'reset', -> 166 | 167 | before -> 168 | @tmpdir = __dirname + '/tmp2' 169 | try 170 | fs.mkdirSync @tmpdir 171 | catch e 172 | 173 | @generator = new ConfigJSONGenerator(__dirname + '/sample-configs') 174 | @generator.destinationPath = @tmpdir 175 | 176 | after -> 177 | fs.rmdirSync @tmpdir 178 | 179 | it 'removes all generated files', -> 180 | 181 | @generator.generate() 182 | assert fs.readdirSync(@tmpdir).length is 4 183 | 184 | @generator.reset() 185 | assert fs.readdirSync(@tmpdir).length is 0 186 | 187 | -------------------------------------------------------------------------------- /spec/lib/custom-configs.coffee: -------------------------------------------------------------------------------- 1 | 2 | CustomConfigs = require '../../src/lib/custom-configs' 3 | 4 | describe 'CustomConfigs', -> 5 | 6 | describe 'loadEnvDir', -> 7 | 8 | it 'loads env dir if exists', -> 9 | configDir = __dirname + '/music-live-configs' 10 | configs = new CustomConfigs().loadEnvDir(configDir, 'development') 11 | assert configs.hasOwnProperty 'server' 12 | assert not configs.hasOwnProperty 'plain' 13 | 14 | it 'returns empty object if not exists', -> 15 | configDir = __dirname + '/music-live-configs' 16 | configs = new CustomConfigs().loadEnvDir(configDir, 'xxx') 17 | assert.deepEqual configs, {} 18 | 19 | 20 | describe 'appendCommonConfigs', -> 21 | 22 | it 'appends common dir if env config does not have the key', -> 23 | configDir = __dirname + '/music-live-configs' 24 | configs = 25 | server: 26 | port: 8080 27 | new CustomConfigs().appendCommonConfigs(configDir, configs) 28 | 29 | assert configs.hasOwnProperty 'plain' 30 | assert configs.server.port is 8080 31 | 32 | 33 | describe 'toObject,', -> 34 | 35 | describe 'when instance is create from config object', -> 36 | it 'contains copy of the given object', -> 37 | configObj = {abc: true} 38 | customConfigs = new CustomConfigs(configObj) 39 | assert customConfigs.toObject().abc is true 40 | 41 | describe 'when instance is create from config object', -> 42 | it 'contains config in the directory', -> 43 | 44 | configDir = __dirname + '/music-live-configs' 45 | customConfigs = new CustomConfigs(configDir) 46 | assert customConfigs.toObject().hasOwnProperty 'plain' 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spec/lib/loopback-boot-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | 4 | LoopbackBootGenerator = require '../../src/lib/loopback-boot-generator' 5 | 6 | describe 'LoopbackBootGenerator', -> 7 | 8 | describe 'generate', -> 9 | 10 | it 'copies javascript files of existing custom role function to loopback/server/custom-roles', -> 11 | 12 | params = 13 | customRoles: 14 | foo: __dirname + '/../custom-roles/abc.js' 15 | 16 | bootGenerator = new LoopbackBootGenerator(params) 17 | 18 | bootGenerator.generate() 19 | 20 | assert fs.existsSync(__dirname + '/../../loopback/server/custom-roles/foo.js') is true 21 | 22 | 23 | it 'does not copy non-existing javascript files', -> 24 | 25 | params = 26 | customRoles: 27 | bar: __dirname + '/../custom-roles/xxx.js' 28 | 29 | bootGenerator = new LoopbackBootGenerator(params) 30 | 31 | bootGenerator.generate() 32 | 33 | assert fs.existsSync(__dirname + '/../../loopback/server/custom-roles/bar.js') is false 34 | 35 | 36 | describe 'reset', -> 37 | 38 | it 'removes javascript files in loopback/server/custom-roles', -> 39 | 40 | assert fs.existsSync(__dirname + '/../../loopback/server/custom-roles/foo.js') is true 41 | 42 | bootGenerator = new LoopbackBootGenerator() 43 | bootGenerator.reset() 44 | 45 | assert fs.existsSync(__dirname + '/../../loopback/server/custom-roles/foo.js') is false 46 | 47 | 48 | -------------------------------------------------------------------------------- /spec/lib/loopback-info.coffee: -------------------------------------------------------------------------------- 1 | 2 | LoopbackInfo = require '../../src/lib/loopback-info' 3 | Main = require '../../src/main' 4 | 5 | fs = require 'fs-extra' 6 | 7 | configDir = __dirname + '/music-live-configs' 8 | 9 | modelDefinitions = {} 10 | 11 | 12 | describe 'LoopbackInfo', -> 13 | 14 | before -> 15 | env = 'xxxyyyzzz' 16 | main = new Main(modelDefinitions, configDir, env) 17 | main.configJSONGenerator.destinationPath = __dirname + '/lbi-test/config' 18 | main.modelsGenerator.destinationDir = __dirname + '/lbi-test/models' 19 | main.modelsGenerator.modelConfigGenerator.destinationPath = __dirname + '/lbi-test/config' 20 | main.modelsGenerator.buildInfoGenerator = __dirname + '/lbi-test/config' 21 | 22 | fs.mkdirsSync __dirname + '/lbi-test/config' 23 | fs.mkdirsSync __dirname + '/lbi-test/models' 24 | 25 | @generated = main.generate() 26 | @lbInfo = new LoopbackInfo({}, @generated) 27 | 28 | after -> 29 | fs.removeSync __dirname + '/lbi-test' 30 | 31 | describe 'getURL', -> 32 | 33 | it 'returns URL with host, port and api root info', -> 34 | assert @lbInfo.getURL() is('localhost:3000/api') 35 | 36 | 37 | describe 'getEnv', -> 38 | 39 | it 'returns environment in which main generated', -> 40 | assert @lbInfo.getEnv() is 'xxxyyyzzz' 41 | 42 | -------------------------------------------------------------------------------- /spec/lib/loopback-server.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | LoopbackServer = require '../../src/lib/loopback-server' 5 | Main = require '../../src/main' 6 | 7 | 8 | describe 'LoopbackServer', -> 9 | 10 | before -> 11 | @main = new Main({}, server: port: 3001) 12 | @main.reset() 13 | @main.generate() 14 | 15 | after -> 16 | @main.reset() 17 | 18 | 19 | describe 'launch', -> 20 | 21 | it 'runs loopback in the same process', -> 22 | @timeout 30000 23 | 24 | launcher = new LoopbackServer() 25 | launcher.launch() 26 | -------------------------------------------------------------------------------- /spec/lib/model-config-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | fs = require 'fs-extra' 5 | 6 | ModelConfigGenerator = require '../../src/lib/model-config-generator' 7 | 8 | 9 | describe 'ModelConfigGenerator', -> 10 | 11 | 12 | describe 'getDestinationPathByName', -> 13 | 14 | it 'returns model-config.json', -> 15 | generator = new ModelConfigGenerator() 16 | path = generator.getDestinationPathByName('model-config') 17 | assert path is normalize __dirname + '/../../loopback/server/model-config.json' 18 | 19 | 20 | describe 'loadDefaultConfig', -> 21 | 22 | it 'loads model-config', -> 23 | config = new ModelConfigGenerator().loadDefaultConfig('model-config') 24 | assert typeof config is 'object' 25 | assert Object.keys(config).length is 10 26 | 27 | 28 | describe 'customConfigObj', -> 29 | 30 | it 'contains model config for each entity names', -> 31 | entityNames = [ 32 | 'player' 33 | 'instrument' 34 | 'song' 35 | ] 36 | config = new ModelConfigGenerator(entityNames).customConfigObj['model-config'] 37 | assert Object.keys(config).length is 3 38 | assert config.player.dataSource is 'db' 39 | assert config.player.public is true 40 | 41 | 42 | describe 'getMergedConfig', -> 43 | 44 | it 'returns model config for each entity names', -> 45 | entityNames = [ 46 | 'player' 47 | 'instrument' 48 | 'song' 49 | ] 50 | config = new ModelConfigGenerator(entityNames).getMergedConfig('model-config') 51 | assert Object.keys(config).length is 10 + 3 52 | 53 | 54 | describe 'generate', -> 55 | 56 | before -> 57 | @generator = new ModelConfigGenerator(['e1', 'e2']) 58 | @generator.destinationPath = __dirname + '/d' 59 | fs.mkdirsSync __dirname + '/d' 60 | 61 | after -> 62 | fs.removeSync __dirname + '/d' 63 | 64 | 65 | it 'returns model config', -> 66 | 67 | generated = @generator.generate() 68 | assert Object.keys(generated).length is 10 + 2 69 | 70 | 71 | -------------------------------------------------------------------------------- /spec/lib/model-definition.coffee: -------------------------------------------------------------------------------- 1 | 2 | ModelDefinition = require '../../src/lib/model-definition' 3 | 4 | 5 | describe 'ModelDefinition', -> 6 | 7 | describe 'constructor', -> 8 | 9 | it 'use custom acls setting if exists', -> 10 | customDefinition = 11 | acls: ['xxx'] 12 | def = new ModelDefinition('entity-model', customDefinition) 13 | assert.deepEqual def.definition.acls, ['xxx'] 14 | 15 | 16 | it 'set acls by aclType', -> 17 | customDefinition = aclType: 'owner' 18 | def = new ModelDefinition('entity-model', customDefinition) 19 | assert def.definition.acls instanceof Array 20 | assert def.definition.acls.length > 1 21 | 22 | 23 | it 'use custom relations setting', -> 24 | customDefinition = 25 | relations: 'xxx' 26 | def = new ModelDefinition('entity-model', customDefinition) 27 | assert def.definition.relations is 'xxx' 28 | 29 | 30 | describe 'isUser', -> 31 | 32 | it 'returns false by default', -> 33 | modelDefinition = new ModelDefinition('xxx') 34 | assert modelDefinition.isUser() is false 35 | 36 | it 'returns true if base is User', -> 37 | modelDefinition = new ModelDefinition('xxx', base: 'User') 38 | assert modelDefinition.isUser() is true 39 | 40 | it 'returns true if base is not User', -> 41 | modelDefinition = new ModelDefinition('xxx', base: 'Users') 42 | assert modelDefinition.isUser() is false 43 | 44 | 45 | describe 'aclType', -> 46 | 47 | it 'is admin by default', -> 48 | modelDefinition = new ModelDefinition('xxx') 49 | assert modelDefinition.aclType is 'admin' 50 | 51 | it 'follows customDefinition value', -> 52 | modelDefinition = new ModelDefinition('xxx', aclType: 'public-read') 53 | assert modelDefinition.aclType is 'public-read' 54 | 55 | 56 | it 'returns "custom" when aclType is not set and acls exist', -> 57 | modelDefinition = new ModelDefinition('xxx', acls: []) 58 | assert modelDefinition.aclType is 'custom' 59 | 60 | 61 | describe 'toJSON', -> 62 | 63 | before -> 64 | @def = new ModelDefinition('entity-model', base: 'User') 65 | @json = @def.toJSON() 66 | 67 | it 'has name', -> 68 | assert @json.name is 'entity-model' 69 | 70 | it 'has plural', -> 71 | assert @json.plural is 'entity-model' 72 | 73 | it 'has base = User', -> 74 | assert @json.base is 'User' 75 | 76 | it 'has idInjection', -> 77 | assert @json.idInjection is true 78 | 79 | it 'has acls', -> 80 | assert @json.hasOwnProperty 'acls' 81 | assert @json.acls instanceof Array 82 | 83 | it 'has relations', -> 84 | assert @json.hasOwnProperty 'relations' 85 | 86 | 87 | describe 'toStringifiedJSON', -> 88 | 89 | it 'returns stringified definition', -> 90 | 91 | def = new ModelDefinition('xxx') 92 | stringifiedJSON = def.toStringifiedJSON() 93 | -> JSON.parse(stringifiedJSON) 94 | 95 | parsed = JSON.parse(stringifiedJSON) 96 | assert.deepEqual parsed, def.toJSON() 97 | -------------------------------------------------------------------------------- /spec/lib/models-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | fs = require 'fs-extra' 4 | 5 | ModelsGenerator = require '../../src/lib/models-generator' 6 | ModelDefinition = require '../../src/lib/model-definition' 7 | ModelConfigGenerator = require '../../src/lib/model-config-generator' 8 | 9 | describe 'ModelsGenerator', -> 10 | 11 | describe 'constructor', -> 12 | before -> 13 | { @createModelDefinitions } = ModelsGenerator:: 14 | ModelsGenerator::createModelDefinitions = -> 15 | model1: true 16 | model2: true 17 | 18 | after -> 19 | ModelsGenerator::createModelDefinitions = @createModelDefinitions 20 | 21 | it 'generate ModelConfigGenerator with array of models', -> 22 | mGenerator = new ModelsGenerator() 23 | assert mGenerator.modelConfigGenerator instanceof ModelConfigGenerator 24 | 25 | 26 | describe 'createModelDefinitions', -> 27 | 28 | it 'creates models included in customDefinitions', -> 29 | 30 | customDefinitions = a: {} 31 | defs = new ModelsGenerator().createModelDefinitions(customDefinitions) 32 | assert defs.hasOwnProperty 'a' 33 | assert not defs.hasOwnProperty 'b' 34 | 35 | 36 | describe 'modelConfigGenerator', -> 37 | 38 | it 'has model config with models included in customDefinitions', -> 39 | 40 | customDefinitions = a: {} 41 | mcGenerator = new ModelsGenerator(customDefinitions).modelConfigGenerator 42 | mergedConfig = mcGenerator.getMergedConfig('model-config') 43 | assert mergedConfig.hasOwnProperty 'a' 44 | assert not mergedConfig.hasOwnProperty 'b' 45 | 46 | describe 'getEmptyJSContent', -> 47 | 48 | it 'returns valid JS code', -> 49 | 50 | vm = require 'vm' 51 | 52 | mGenerator = new ModelsGenerator() 53 | context = vm.createContext module: {} 54 | 55 | vm.runInContext(mGenerator.getEmptyJSContent(), context) 56 | 57 | describe 'getJSContent', -> 58 | 59 | it 'return valid js code with empty array validations', -> 60 | mGenerator = new ModelsGenerator() 61 | definition = [] 62 | result = mGenerator.getJSContent(definition) 63 | # just exists function placeholder 64 | assert /module\.exports = function\(Model\) \{/.test(result) 65 | 66 | it 'return valid js code with empty validations', -> 67 | mGenerator = new ModelsGenerator() 68 | result = mGenerator.getJSContent(null) 69 | # just exists function placeholder 70 | assert /module\.exports = function\(Model\) \{/.test(result) 71 | 72 | it 'return valid js code with validation methods', -> 73 | 74 | mGenerator = new ModelsGenerator() 75 | definition = [ 76 | username: 77 | required: true 78 | min: 6, 79 | max: 10, 80 | pattern: '^[a-z]' 81 | ] 82 | result = mGenerator.getJSContent(definition) 83 | assert /validatesPresenceOf\('username'\)/.test(result) 84 | assert /validatesFormatOf\('username', \{ with: \/\^\[a-z\]\/ \}\)/.test(result) 85 | assert /validatesLengthOf\('username', \{ max: 10 \}\)/.test(result) 86 | assert /validatesLengthOf\('username', \{ min: 6 \}\)/.test(result) 87 | 88 | describe 'generateJSONandJS', -> 89 | 90 | before -> 91 | @generator = new ModelsGenerator() 92 | @generator.destinationDir = __dirname + '/a/b/c' 93 | 94 | fs.mkdirsSync @generator.destinationDir 95 | 96 | @modelName = 'test-model' 97 | @contents = test: true 98 | 99 | @generator.generateJSONandJS(@modelName, @contents) 100 | 101 | after -> 102 | fs.removeSync __dirname + '/a' 103 | 104 | it 'generate JSON file', -> 105 | assert fs.existsSync(@generator.destinationDir + '/test-model.json') is true 106 | assert.deepEqual require(@generator.destinationDir + '/test-model.json'), {test: true} 107 | 108 | it 'generate JS file', -> 109 | assert fs.existsSync(@generator.destinationDir + '/test-model.json') is true 110 | content = fs.readFileSync(@generator.destinationDir + '/test-model.js', 'utf8') 111 | assert content is @generator.getEmptyJSContent() 112 | 113 | describe 'generateJSONandJS, when give the validation define', -> 114 | 115 | before -> 116 | @generator = new ModelsGenerator() 117 | @generator.destinationDir = __dirname + '/a/b/c' 118 | 119 | fs.mkdirsSync @generator.destinationDir 120 | 121 | @modelName = 'test-model' 122 | @definition = 123 | patient: 124 | name: 'patient', 125 | plural: 'patient', 126 | properties: 127 | email: 128 | required: false 129 | username: 130 | type: 'string' 131 | required: true 132 | validations: [ 133 | username: 134 | max: 10 135 | min: 6 136 | ] 137 | 138 | modelDefinition = @generator.createModelDefinitions @definition 139 | @generator.generateJSONandJS(@modelName, modelDefinition.patient) 140 | 141 | after -> 142 | fs.removeSync __dirname + '/a' 143 | 144 | it 'generate non empty JS file', -> 145 | content = fs.readFileSync(@generator.destinationDir + '/test-model.js', 'utf8') 146 | assert /validatesLengthOf/.test(content) 147 | assert /'username', { min: 6 }/.test(content) 148 | 149 | describe 'generateBuiltinModels', -> 150 | 151 | before -> 152 | @generator = new ModelsGenerator() 153 | @generator.destinationDir = __dirname + '/b/c/d' 154 | 155 | fs.mkdirsSync @generator.destinationDir 156 | 157 | @modelName = 'test-model' 158 | @contents = JSON.stringify test: true 159 | 160 | @generator.generateBuiltinModels(@modelName, @contents) 161 | 162 | after -> 163 | fs.removeSync __dirname + '/b' 164 | 165 | it 'generate four JSON files', -> 166 | assert fs.readdirSync(@generator.destinationDir).length is 8 167 | 168 | 169 | it 'generate JS file', -> 170 | assert fs.readdirSync(@generator.destinationDir).length is 8 171 | 172 | 173 | describe 'generateDefinitionFiles', -> 174 | 175 | describe 'reset', -> 176 | 177 | before -> 178 | @generator = new ModelsGenerator() 179 | @generator.destinationDir = __dirname + '/c' 180 | @generator.modelConfigGenerator.destinationPath = __dirname + '/c' 181 | fs.mkdirsSync __dirname + '/c' 182 | 183 | 184 | it 'remove dir if exists', -> 185 | @generator.reset() 186 | assert fs.existsSync(@generator.destinationDir) is false 187 | 188 | it 'do nothing if dir does not exist', -> 189 | => @generator.reset() 190 | 191 | 192 | describe 'generate', -> 193 | 194 | before -> 195 | @generator = new ModelsGenerator() 196 | @generator.destinationDir = __dirname + '/d' 197 | @generator.modelConfigGenerator.destinationPath = __dirname + '/d' 198 | fs.mkdirsSync __dirname + '/d' 199 | 200 | after -> 201 | fs.removeSync __dirname + '/d' 202 | 203 | it 'returns generated models and configs', -> 204 | 205 | generated = @generator.generate() 206 | 207 | assert generated.hasOwnProperty 'config' 208 | assert generated.hasOwnProperty 'names' 209 | assert generated.names instanceof Array 210 | assert generated.names.length is 4 211 | assert typeof generated.config is 'object' 212 | 213 | describe 'generate, when give the relation define, and owner permission is read only', -> 214 | 215 | before -> 216 | 217 | ownerPermission = 'r' 218 | 219 | define = 220 | staff: 221 | aclType: 222 | owner: 'rwx' 223 | name: 'staff' 224 | plural: 'staff' 225 | base: 'User' 226 | idInjection: true 227 | properties: {} 228 | validations: [] 229 | relations: 230 | 'job-with-staffId': 231 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 232 | job: 233 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 234 | job: 235 | aclType: 236 | owner: ownerPermission 237 | name: 'job', 238 | plural: 'job', 239 | base: 'PersistedModel', 240 | idInjection: true, 241 | properties: {}, 242 | validations: [], 243 | relations: 244 | staff: 245 | type: 'belongsTo', model: 'staff', foreignKey: 'staffId' 246 | 247 | 248 | @generator = new ModelsGenerator(define) 249 | @generator.destinationDir = __dirname + '/d' 250 | @generator.modelConfigGenerator.destinationPath = __dirname + '/d' 251 | fs.mkdirsSync __dirname + '/d' 252 | 253 | after -> 254 | fs.removeSync __dirname + '/d' 255 | 256 | it 'generate JSON file include related models', (done) -> 257 | 258 | @generator.generate() 259 | 260 | fs.readFile __dirname + '/d/staff.json', 'utf8', (err, data) -> 261 | 262 | acls = JSON.parse(data).acls 263 | 264 | acl = [ 265 | accessType : "WRITE" 266 | permission : "DENY" 267 | principalId : "$owner" 268 | principalType : "ROLE" 269 | property : "__create__job" 270 | , 271 | accessType : "WRITE" 272 | permission : "DENY" 273 | principalId : "$owner" 274 | principalType : "ROLE" 275 | property : "__delete__job" 276 | , 277 | accessType : "WRITE" 278 | permission : "DENY" 279 | principalId : "$owner" 280 | principalType : "ROLE" 281 | property : "__destroyById__job" 282 | , 283 | accessType : "WRITE" 284 | permission : "DENY" 285 | principalId : "$owner" 286 | principalType : "ROLE" 287 | property : "__updateById__job" 288 | ] 289 | 290 | assert.deepEqual acls.splice(8, 4), acl 291 | 292 | done() 293 | 294 | describe 'generate, when give the relation define, and owner permission is undefined', -> 295 | 296 | before -> 297 | 298 | # job aclType = '' 299 | define = 300 | staff: 301 | aclType: 302 | owner: 'rwx' 303 | name: 'staff' 304 | plural: 'staff' 305 | base: 'User' 306 | idInjection: true 307 | properties: {} 308 | validations: [] 309 | relations: 310 | 'job-with-staffId': 311 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 312 | job: 313 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 314 | job: 315 | aclType: '' 316 | name: 'job', 317 | plural: 'job', 318 | base: 'PersistedModel', 319 | idInjection: true, 320 | properties: {}, 321 | validations: [], 322 | relations: 323 | staff: 324 | type: 'belongsTo', model: 'staff', foreignKey: 'staffId' 325 | 326 | 327 | @generator = new ModelsGenerator(define) 328 | @generator.destinationDir = __dirname + '/d' 329 | @generator.modelConfigGenerator.destinationPath = __dirname + '/d' 330 | fs.mkdirsSync __dirname + '/d' 331 | 332 | after -> 333 | fs.removeSync __dirname + '/d' 334 | 335 | it 'generate JSON file include related models', (done) -> 336 | 337 | @generator.generate() 338 | 339 | fs.readFile __dirname + '/d/staff.json', 'utf8', (err, data) -> 340 | 341 | acls = JSON.parse(data).acls 342 | 343 | acl = [ 344 | accessType : "WRITE" 345 | permission : "DENY" 346 | principalId : "$owner" 347 | principalType : "ROLE" 348 | property : "__create__job" 349 | , 350 | accessType : "WRITE" 351 | permission : "DENY" 352 | principalId : "$owner" 353 | principalType : "ROLE" 354 | property : "__delete__job" 355 | , 356 | accessType : "WRITE" 357 | permission : "DENY" 358 | principalId : "$owner" 359 | principalType : "ROLE" 360 | property : "__destroyById__job" 361 | , 362 | accessType : "WRITE" 363 | permission : "DENY" 364 | principalId : "$owner" 365 | principalType : "ROLE" 366 | property : "__updateById__job" 367 | ] 368 | 369 | assert.deepEqual acls.splice(8, 4), acl 370 | 371 | done() 372 | 373 | 374 | describe 'generate, when give the relation define, and owner permission is read-write', -> 375 | 376 | before -> 377 | 378 | ownerPermission = 'rw' 379 | 380 | define = 381 | staff: 382 | aclType: 383 | owner: 'rwx' 384 | name: 'staff' 385 | plural: 'staff' 386 | base: 'User' 387 | idInjection: true 388 | properties: {} 389 | validations: [] 390 | relations: 391 | 'job-with-staffId': 392 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 393 | job: 394 | type: 'hasMany', model: 'staff', foreignKey: 'staffId' 395 | job: 396 | aclType: 397 | owner: ownerPermission 398 | name: 'job', 399 | plural: 'job', 400 | base: 'PersistedModel', 401 | idInjection: true, 402 | properties: {}, 403 | validations: [], 404 | relations: 405 | staff: 406 | type: 'belongsTo', model: 'staff', foreignKey: 'staffId' 407 | 408 | 409 | @generator = new ModelsGenerator(define) 410 | @generator.destinationDir = __dirname + '/d' 411 | @generator.modelConfigGenerator.destinationPath = __dirname + '/d' 412 | fs.mkdirsSync __dirname + '/d' 413 | 414 | after -> 415 | fs.removeSync __dirname + '/d' 416 | 417 | it 'generate JSON file does not include related models', (done) -> 418 | 419 | @generator.generate() 420 | 421 | fs.readFile __dirname + '/d/staff.json', 'utf8', (err, data) -> 422 | 423 | acls = JSON.parse(data).acls 424 | 425 | acl = [] 426 | 427 | assert.deepEqual acls.splice(8, 4), acl 428 | 429 | done() 430 | -------------------------------------------------------------------------------- /spec/lib/music-live-configs/common/plain.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | key1: 'from common' 4 | key2: 'from common' 5 | -------------------------------------------------------------------------------- /spec/lib/music-live-configs/common/server.json: -------------------------------------------------------------------------------- 1 | { "port": 3000 } 2 | -------------------------------------------------------------------------------- /spec/lib/music-live-configs/development/server.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | 4 | port: 8080 5 | -------------------------------------------------------------------------------- /spec/lib/music-live-configs/local/plain.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | key1: 'from local' 4 | key2: 'from local' 5 | -------------------------------------------------------------------------------- /spec/lib/music-live-configs/model-definitions.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | 4 | song: 5 | aclType: 'public-read' 6 | 7 | player: 8 | base: 'User' 9 | aclType: 'admin' 10 | 11 | instrument: 12 | aclType: 'owner' 13 | 14 | -------------------------------------------------------------------------------- /spec/lib/sample-configs/common/plain.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | key1: 'from common' 4 | key2: 'from common' 5 | -------------------------------------------------------------------------------- /spec/lib/sample-configs/local/plain.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | key1: 'from local' 4 | key2: 'from local' 5 | -------------------------------------------------------------------------------- /spec/main.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | fs = require 'fs-extra' 4 | 5 | Main = require '../src/main' 6 | LoopbackServer = require '../src/lib/loopback-server' 7 | 8 | modelDefinitions = {} 9 | 10 | configDir = normalize __dirname + '/lib/music-live-configs' 11 | 12 | describe 'Main', -> 13 | 14 | describe 'env', -> 15 | 16 | it 'is "development" by default', -> 17 | 18 | main = new Main(modelDefinitions, configDir) 19 | assert main.env is 'development' 20 | 21 | 22 | it 'is set the same as environment variable "NODE_ENV" if set.', -> 23 | 24 | process.env.NODE_ENV = 'xxxx' 25 | 26 | main = new Main(modelDefinitions, configDir) 27 | assert main.env is 'xxxx' 28 | 29 | process.env.NODE_ENV = '' 30 | 31 | 32 | it 'is set value from constructor if set.', -> 33 | process.env.NODE_ENV = 'xxxx' 34 | main = new Main(modelDefinitions, configDir, 'local') 35 | 36 | assert main.env is 'local' 37 | process.env.NODE_ENV = '' 38 | 39 | 40 | describe 'generate', -> 41 | 42 | it 'invokes four generator\'s generate()', -> 43 | 44 | main = new Main(modelDefinitions, configDir) 45 | counter = 0 46 | generate = -> counter++ 47 | main.configJSONGenerator = generate: generate 48 | main.modelsGenerator = generate: generate 49 | main.buildInfoGenerator = generate: generate 50 | main.bootGenerator = generate: generate 51 | 52 | main.generate() 53 | 54 | assert counter is 4 55 | 56 | 57 | it 'returns generated contents', -> 58 | 59 | fs.mkdirsSync(__dirname + '/main-test/config') 60 | fs.mkdirsSync(__dirname + '/main-test/models') 61 | 62 | main = new Main(modelDefinitions, configDir) 63 | main.configJSONGenerator.destinationPath = __dirname + '/main-test/config' 64 | main.modelsGenerator.destinationDir = __dirname + '/main-test/models' 65 | main.modelsGenerator.modelConfigGenerator.destinationPath = __dirname + '/main-test/config' 66 | main.modelsGenerator.buildInfoGenerator = __dirname + '/main-test/config' 67 | 68 | generated = main.generate() 69 | 70 | assert generated.hasOwnProperty('config') 71 | assert generated.hasOwnProperty('buildInfo') 72 | assert generated.hasOwnProperty('models') 73 | assert generated.hasOwnProperty('bootInfo') 74 | 75 | fs.removeSync __dirname + '/main-test' 76 | 77 | 78 | describe 'reset', -> 79 | 80 | it 'invokes four generator\'s reset()', -> 81 | 82 | main = new Main(modelDefinitions, configDir) 83 | counter = 0 84 | reset = -> counter++ 85 | main.configJSONGenerator = reset: reset 86 | main.modelsGenerator = reset: reset 87 | main.buildInfoGenerator = reset: reset 88 | main.bootGenerator = reset: reset 89 | 90 | main.reset() 91 | 92 | assert counter is 4 93 | 94 | 95 | describe '@run', -> 96 | 97 | beforeEach -> 98 | @called = {} 99 | 100 | @reset = Main::reset 101 | @generate = Main::generate 102 | @launchLoopback = Main.launchLoopback 103 | 104 | Main::reset = => @called.reset = true 105 | Main::generate = => @called.generate = true 106 | Main.launchLoopback = (params) => Promise.resolve @called.launchLoopback = true 107 | 108 | afterEach -> 109 | Main::reset = @reset 110 | Main::generate = @generate 111 | Main.launchLoopback = @launchLoopback 112 | 113 | it 'invokes reset() unless reset is false', -> 114 | 115 | Main.run(modelDefinitions, configDir) 116 | 117 | assert @called.reset is true 118 | assert @called.generate is true 119 | assert @called.launchLoopback is true 120 | 121 | 122 | it 'does not invoke reset if reset is false', -> 123 | 124 | Main.run(modelDefinitions, configDir, reset: false) 125 | 126 | assert not @called.reset? 127 | assert @called.generate is true 128 | assert @called.launchLoopback is true 129 | 130 | 131 | it 'invokes launchLoopback with `admin` options', -> 132 | 133 | adminOptions = 134 | id: 'admin-1234' 135 | email: 'i-am-admin@example.com' 136 | password: 'administrator' 137 | intervalHours: 12 138 | 139 | params = null 140 | 141 | Main.launchLoopback = (p) => 142 | params = p 143 | Promise.resolve @called.launchLoopback = true 144 | 145 | Main.run(modelDefinitions, configDir, admin: adminOptions) 146 | 147 | assert @called.reset is true 148 | assert @called.generate is true 149 | assert @called.launchLoopback is true 150 | assert params is adminOptions 151 | 152 | -------------------------------------------------------------------------------- /src/lib/acl-conditions.coffee: -------------------------------------------------------------------------------- 1 | 2 | class AclConditions 3 | 4 | ###* 5 | basic names 6 | @static 7 | ### 8 | @basicNames: ['public', 'member', 'owner'] 9 | 10 | 11 | ###* 12 | get array of ['READ', 'WRITE', 'EXECUTE'] 13 | 14 | @method regularPermissions 15 | @static 16 | @param {String} rwx characters sequence 'r w x' meaning READ, WRITE, EXECUTE 17 | @param {Object} flags if flags.r is on, remove READ, and so the others. 18 | @return {Array(String)} 19 | ### 20 | @regularPermissions: (rwx = '', flags = {}) -> 21 | 22 | permissions = { r: 'READ', w: 'WRITE', x: 'EXECUTE' } 23 | 24 | return rwx.split('') 25 | .filter (c) -> not flags[c] 26 | .filter (c) -> permissions[c] 27 | .map (c) -> 28 | flags[c] = true 29 | return permissions[c] 30 | 31 | 32 | constructor: (aclType = {}) -> 33 | 34 | @basicPermissions = {} 35 | @customPermissions = {} 36 | 37 | flags = { r: false, w: false, x: false } 38 | 39 | # set basic rwx 40 | for name in @constructor.basicNames 41 | rwx = aclType[name] 42 | @basicPermissions[name] = @constructor.regularPermissions(rwx, flags) 43 | 44 | # set custom rwx 45 | for name, rwx of aclType when name not in @constructor.basicNames 46 | @customPermissions[name] = @constructor.regularPermissions(rwx) 47 | 48 | 49 | isPublic: -> 50 | @basicPermissions.public?.length is 3 51 | 52 | 53 | isAdminOnly: -> 54 | return false if Object.keys(@customPermissions).length > 0 55 | 56 | for name, regularPermissions of @basicPermissions 57 | return false if regularPermissions.length > 0 58 | return true 59 | 60 | 61 | module.exports = AclConditions 62 | -------------------------------------------------------------------------------- /src/lib/acl-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | AclConditions = require './acl-conditions' 3 | 4 | ###* 5 | generate ACL 6 | ACL is Array of access control information 7 | 8 | @class AclGenerator 9 | ### 10 | class AclGenerator 11 | 12 | constructor: (aclType = 'admin', @isUser = false, @relationDefinitions = {}) -> 13 | @acl = [] 14 | @aclConditions = @constructor.createAclConditions(aclType) 15 | 16 | 17 | ###* 18 | create AclConditions by aclType 19 | @param {String|Object} aclType 20 | ### 21 | @createAclConditions: (aclType) -> 22 | 23 | if typeof aclType is 'string' 24 | 25 | aclTypeStr = aclType 26 | 27 | switch aclTypeStr 28 | when 'admin' 29 | aclType = {} 30 | 31 | when 'owner' 32 | aclType = { owner: 'rwx' } 33 | 34 | when 'public-read-by-owner' 35 | aclType = { public: 'r', owner: 'rwx' } 36 | 37 | when 'member-read-by-owner' 38 | aclType = { member: 'r', owner: 'rwx' } 39 | 40 | when 'member-read' 41 | aclType = { member: 'r' } 42 | 43 | when 'public-read' 44 | aclType = { public: 'r' } 45 | 46 | when 'none' 47 | aclType = { public: 'rwx' } 48 | 49 | return new AclConditions(aclType) 50 | 51 | 52 | ###* 53 | get ACL by aclConditions 54 | 55 | @method generate 56 | @public 57 | return {Array} ACL 58 | ### 59 | generate: -> 60 | 61 | if @aclConditions.isPublic() 62 | return @acl 63 | 64 | @commonACL() 65 | 66 | if @aclConditions.isAdminOnly() 67 | @adminACL() 68 | return @acl 69 | 70 | @addAllowACL('$everyone', @aclConditions.basicPermissions.public) 71 | @addAllowACL('$authenticated', @aclConditions.basicPermissions.member) 72 | @addAllowACL('$owner', @aclConditions.basicPermissions.owner) 73 | 74 | # deny write of related model if read only permission 75 | for roleName, relationDefine of @relationDefinitions 76 | if relationDefine.aclType.owner? 77 | rwx = relationDefine.aclType.owner 78 | accessTypes = AclConditions.regularPermissions(rwx) 79 | else 80 | accessTypes = ['READ'] 81 | 82 | if accessTypes.length is 1 and accessTypes[0] is 'READ' 83 | @addDenyACL('$owner', ['WRITE'], @getRestrictingProperties(relationDefine.type, 'WRITE', roleName)) 84 | 85 | for roleName, accessTypes of @aclConditions.customPermissions 86 | @addAllowACL(roleName, accessTypes, ['create', 'updateAttributes', 'upsert', 'deleteById']) 87 | 88 | return @acl 89 | 90 | 91 | ###* 92 | append ACL allowing accesses from the accessToken of model's owners 93 | 94 | @method ownerACL 95 | @private 96 | ### 97 | addAllowACL: (principalId, accessTypes, properties = []) -> 98 | 99 | accessTypes.forEach (accessType) => 100 | if properties.length > 0 and accessType is 'WRITE' 101 | for property in properties 102 | @acl.push 103 | accessType: accessType 104 | principalType: 'ROLE' 105 | principalId: principalId 106 | permission: 'ALLOW' 107 | property: property 108 | else 109 | @acl.push 110 | accessType: accessType 111 | principalType: 'ROLE' 112 | principalId: principalId 113 | permission: 'ALLOW' 114 | 115 | ###* 116 | append ACL denying accesses from the accessToken of model's owners 117 | 118 | @method addDenyACL 119 | @param principalId 120 | @param accessTypes 121 | @param properties 122 | @private 123 | ### 124 | addDenyACL: (principalId, accessTypes, properties = []) -> 125 | 126 | accessTypes.forEach (accessType) => 127 | for property in properties 128 | @acl.push 129 | accessType: accessType 130 | principalType: 'ROLE' 131 | principalId: principalId 132 | permission: 'DENY' 133 | property: property 134 | 135 | 136 | ###* 137 | append basic ACL, which allow accesses only from admin 138 | 139 | @method commonACL 140 | @private 141 | ### 142 | commonACL: -> 143 | # set everyone access denied and admin access allowed 144 | @acl.push 145 | accessType: '*' 146 | principalType: 'ROLE' 147 | principalId: '$everyone' 148 | permission: 'DENY' 149 | 150 | @acl.push 151 | accessType: '*' 152 | principalType: 'ROLE' 153 | principalId: 'admin' 154 | permission: 'ALLOW' 155 | 156 | if @isUser 157 | @userACL() 158 | @ 159 | 160 | ###* 161 | append ACL for User model 162 | 163 | @method userACL 164 | @private 165 | ### 166 | userACL: -> 167 | # admin cannot logout. avoid CSRF attacks which make admin logout. 168 | @acl.push 169 | accessType: 'EXECUTE' 170 | principalType: 'ROLE' 171 | principalId: 'admin' 172 | permission: 'DENY' 173 | property: 'logout' 174 | 175 | # user creation is denied by default. 176 | @acl.push 177 | accessType: 'WRITE' 178 | principalType: 'ROLE' 179 | principalId: '$everyone' 180 | permission: 'DENY' 181 | property: 'create' 182 | @ 183 | 184 | # user creation is allowed by admin. 185 | @acl.push 186 | accessType: 'WRITE' 187 | principalType: 'ROLE' 188 | principalId: 'admin' 189 | permission: 'ALLOW' 190 | property: 'create' 191 | @ 192 | 193 | 194 | ###* 195 | append ACL allowing accesses only from admin 196 | 197 | @method adminACL 198 | @private 199 | ### 200 | adminACL: -> 201 | if @isUser 202 | @adminUserACL() 203 | 204 | 205 | ###* 206 | append ACL for User model handled by admin, 207 | denying login from everyone. Login must be executed via admin access. 208 | 209 | @method adminUserACL 210 | @private 211 | ### 212 | adminUserACL: -> 213 | # login is denied by default 214 | @acl.push 215 | accessType: 'EXECUTE' 216 | principalType: 'ROLE' 217 | principalId: '$everyone' 218 | permission: 'DENY' 219 | property: 'login' 220 | 221 | @acl.push 222 | accessType: 'EXECUTE' 223 | principalType: 'ROLE' 224 | principalId: 'admin' 225 | permission: 'ALLOW' 226 | property: 'login' 227 | 228 | ###* 229 | get restricting properties 230 | 231 | @method getRestrictingProperties 232 | @param relationType 233 | @param accessTypes 234 | @param roleName 235 | @private 236 | ### 237 | getRestrictingProperties: (relationType, accessType, roleName) -> 238 | 239 | properties = [] 240 | 241 | switch relationType 242 | when 'hasMany' 243 | if accessType is 'WRITE' 244 | properties.push "__create__#{roleName}" 245 | properties.push "__delete__#{roleName}" 246 | properties.push "__destroyById__#{roleName}" 247 | properties.push "__updateById__#{roleName}" 248 | 249 | return properties 250 | 251 | module.exports = AclGenerator 252 | -------------------------------------------------------------------------------- /src/lib/build-info-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | ConfigJSONGenerator = require './config-json-generator' 5 | 6 | class BuildInfoGenerator extends ConfigJSONGenerator 7 | 8 | defaultConfigsPath: normalize "#{__dirname}/../../default-values" 9 | 10 | configNames: [ 'build-info' ] 11 | 12 | 13 | ###* 14 | @constructor 15 | ### 16 | constructor: (@modelDefinitions, @customConfigs, @env) -> 17 | 18 | 19 | getMergedConfig: -> 20 | env : @env 21 | customConfigs : @customConfigs 22 | modelDefinitions : @modelDefinitions 23 | buildAt : new Date().toISOString() 24 | 25 | 26 | generate: -> 27 | generated = super() 28 | return generated['build-info'] 29 | 30 | 31 | 32 | module.exports = BuildInfoGenerator 33 | -------------------------------------------------------------------------------- /src/lib/config-json-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | fs = require 'fs' 5 | 6 | class ConfigJSONGenerator 7 | 8 | defaultConfigsPath: normalize "#{__dirname}/../../default-values/non-model-configs" 9 | destinationPath : normalize "#{__dirname}/../../loopback/server" 10 | 11 | configNames: [ 12 | 'datasources' 13 | 'middleware' 14 | 'server' 15 | 'push-credentials' 16 | ] 17 | 18 | ###* 19 | default-configs/server.json will be server/config.json 20 | 21 | @property destinationNamePairs 22 | @private 23 | ### 24 | destinationNamePairs: 25 | server: 'config' 26 | 27 | 28 | ###* 29 | 30 | @constructor 31 | @param {Object} customConfigObj 32 | @param {String} env 33 | ### 34 | constructor: (@customConfigObj = {}, env) -> 35 | 36 | 37 | ###* 38 | generate JSON files into server dir 39 | 40 | @method generate 41 | @public 42 | @return {Object} generatedContents 43 | ### 44 | generate: -> 45 | 46 | generatedContents = {} 47 | 48 | for configName in @configNames 49 | 50 | config = @getMergedConfig(configName) 51 | 52 | path = @getDestinationPathByName(configName) 53 | 54 | fs.writeFileSync(path, JSON.stringify config, null, 2) 55 | 56 | generatedContents[configName] = config 57 | 58 | return generatedContents 59 | 60 | ###* 61 | remove previously-generated JSON files 62 | 63 | @method reset 64 | @public 65 | ### 66 | reset: -> 67 | for configName in @configNames 68 | path = @getDestinationPathByName(configName) 69 | if fs.existsSync path 70 | fs.unlinkSync(path) 71 | 72 | 73 | 74 | ###* 75 | new config path 76 | ### 77 | getDestinationPathByName: (configName) -> 78 | 79 | filename = @destinationNamePairs[configName] ? configName 80 | 81 | return normalize @destinationPath + '/' + filename + '.json' 82 | 83 | 84 | ###* 85 | merge custom and default for each config names 86 | 87 | @private 88 | ### 89 | getMergedConfig: (configName) -> 90 | 91 | defaultConfig = @loadDefaultConfig(configName) 92 | customConfig = @customConfigObj[configName] 93 | 94 | return @merge customConfig, defaultConfig 95 | 96 | 97 | 98 | ###* 99 | merge two objects into one new object 100 | object at 1st argument overrides that at 2nd 101 | 102 | @param {Object} dominant 103 | @param {Object} base 104 | @return {Object} merged 105 | @private 106 | ### 107 | merge: (dominant = {}, base = {}) -> 108 | 109 | merged = {} 110 | merged[k] = v for own k,v of base 111 | 112 | for own k, sub of dominant 113 | if merged[k]? and typeof merged[k] is 'object' and v? 114 | # merges subobject 115 | merged[k] = @merge sub, merged[k] 116 | else 117 | merged[k] = sub 118 | 119 | return merged 120 | 121 | 122 | ###* 123 | load default config JSON files 124 | 125 | @private 126 | ### 127 | loadDefaultConfig: (configName) -> 128 | 129 | try 130 | require "#{@defaultConfigsPath}/#{configName}.json" 131 | catch e 132 | return null 133 | 134 | 135 | module.exports = ConfigJSONGenerator 136 | -------------------------------------------------------------------------------- /src/lib/custom-configs.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | 4 | class CustomConfigs 5 | 6 | constructor: (configs = {}, env) -> 7 | if typeof configs is 'string' # parse is as a configDir 8 | configDir = configs 9 | @configs = @loadDir(configDir, env) 10 | else 11 | @configs = @clone configs 12 | delete @configs.models 13 | 14 | 15 | toObject: -> 16 | return @clone @configs 17 | 18 | 19 | loadDir: (configDir, env) -> 20 | 21 | configs = @loadEnvDir(configDir, env) 22 | @appendCommonConfigs(configDir, configs) 23 | 24 | return configs 25 | 26 | 27 | loadEnvDir: (configDir, env) -> 28 | configs = {} 29 | envDir = "#{configDir}/#{env}" 30 | 31 | return configs if not env or not fs.existsSync envDir 32 | 33 | for configFile in fs.readdirSync(envDir) 34 | [configName, ext] = configFile.split('.') 35 | configs[configName] = require(envDir + '/' + configFile) if ext in ['coffee', 'js', 'json'] 36 | 37 | return configs 38 | 39 | 40 | 41 | appendCommonConfigs: (configDir, configs) -> 42 | 43 | commonDir = "#{configDir}/common" 44 | return if not fs.existsSync commonDir 45 | 46 | for configFile in fs.readdirSync(commonDir) 47 | [configName, ext] = configFile.split('.') 48 | configs[configName] ?= require(commonDir + '/' + configFile) if ext in ['coffee', 'js', 'json'] 49 | 50 | 51 | 52 | clone: (obj) -> 53 | for k, v of obj 54 | if v? and typeof v is 'object' 55 | obj[k] = @clone(v) 56 | else 57 | obj[k] = v 58 | 59 | return obj 60 | 61 | 62 | module.exports = CustomConfigs 63 | -------------------------------------------------------------------------------- /src/lib/loopback-boot-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | fs = require 'fs' 4 | 5 | class LoopbackBootGenerator 6 | 7 | @dirpath: normalize __dirname + '/../../loopback/server/custom-roles' 8 | 9 | 10 | constructor: (params = {}) -> 11 | 12 | { @customRoles } = params 13 | 14 | generate: -> 15 | 16 | return null if not @customRoles 17 | 18 | for name, filepath of @customRoles 19 | if not fs.existsSync(filepath) 20 | delete @customRoles[name] 21 | else 22 | fs.writeFileSync(@constructor.dirpath + '/' + name + '.js', fs.readFileSync(filepath)) 23 | 24 | return customRoles: @customRoles 25 | 26 | 27 | reset: -> 28 | for filename in fs.readdirSync(@constructor.dirpath) when filename.slice(-3) is '.js' 29 | fs.unlinkSync(@constructor.dirpath + '/' + filename) 30 | 31 | 32 | module.exports = LoopbackBootGenerator 33 | -------------------------------------------------------------------------------- /src/lib/loopback-info.coffee: -------------------------------------------------------------------------------- 1 | 2 | ###* 3 | Loopback info 4 | 5 | @class LoopbackInfo 6 | ### 7 | class LoopbackInfo 8 | 9 | constructor: (@lbServer, generatedInMain = {}) -> 10 | 11 | { @config, @models, @buildInfo, @bootInfo } = generatedInMain 12 | 13 | 14 | ###* 15 | get hosting URL 16 | 17 | @method getURL 18 | @public 19 | @param {String} [hostName] 20 | @return {String} url 21 | ### 22 | getURL: (hostName) -> 23 | hostName ?= @config.server.host 24 | 25 | "#{hostName}:#{@config.server.port}#{@config.server.restApiRoot}" 26 | 27 | 28 | ###* 29 | get available admin access tokens 30 | 31 | @method getAdminTokens 32 | @public 33 | @return {Array(String)} tokens 34 | ### 35 | getAdminTokens: -> 36 | 37 | @lbServer.app.lwaTokenManager.getCurrentTokens() 38 | 39 | 40 | 41 | ###* 42 | get environment 43 | 44 | @method getEnv 45 | @public 46 | @return {String} env 47 | ### 48 | getEnv: -> @buildInfo.env 49 | 50 | 51 | 52 | module.exports = LoopbackInfo 53 | -------------------------------------------------------------------------------- /src/lib/loopback-server.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | AdminTokenManager = require '../server/admin-token-manager' 5 | ParticipantTokenSetter = require '../server/participant-token-setter' 6 | 7 | ###* 8 | launches loopback server 9 | 10 | @class LoopbackServer 11 | ### 12 | class LoopbackServer 13 | 14 | entryPath: normalize __dirname + '/../../loopback/server/server.js' 15 | 16 | ###* 17 | @param {Function|Array(String)} [options.fetch] function to return admin tokens (or promise of it). When string[] is given, these value are used for the admin access token. 18 | @param {String} [options.email=loopback-with-admin@example.com] email address for admin user 19 | @param {String} [options.id=loopback-with-admin-user-id] id of admin user 20 | @param {String} [options.password=admin-user-password] password of admin user 21 | @param {Number} [options.intervalHours] Interval hours to fetch new admin token. 22 | @param {String} [participantToken] static token for participant user 23 | ### 24 | launch: (options = {}, participantToken) -> new Promise (resolve, reject) => 25 | 26 | @app = require(@entryPath) 27 | 28 | # see loopback/server/boot/admin.js 29 | @app.lwaTokenManager = new AdminTokenManager(options) 30 | # see loopback/server/boot/participant.js 31 | if participantToken 32 | @app.participantTokenSetter = new ParticipantTokenSetter(participantToken) 33 | 34 | return @app.start (err) => 35 | 36 | return reject(err) if err 37 | 38 | @startRefreshingAdminTokens(intervalHours = Number(options.intervalHours) || 12) 39 | 40 | resolve() 41 | 42 | 43 | ###* 44 | Start refreshing admin access tokens 45 | 46 | @public 47 | @method startRefreshingAdminTokens 48 | @param {Number} [intervalHours=12] 49 | ### 50 | startRefreshingAdminTokens: (intervalHours = 12) -> 51 | 52 | console.log "Admin token will be refreshed every #{intervalHours} hours." 53 | 54 | clearInterval(@timer) if @timer? 55 | 56 | @timer = setInterval => 57 | 58 | @app.lwaTokenManager.refreshTokens() 59 | 60 | , intervalHours * 3600 * 1000 61 | 62 | 63 | 64 | ###* 65 | Check if the regular timer refreshing admin access tokens is set 66 | 67 | @public 68 | @method isRefreshingAdminTokens 69 | @return {Boolean} 70 | ### 71 | isRefreshingAdminTokens: -> @timer? 72 | 73 | 74 | ###* 75 | Stop refreshing admin access tokens 76 | 77 | @public 78 | @method stopRefreshingAdminTokens 79 | ### 80 | stopRefreshingAdminTokens: -> 81 | 82 | console.log "Admin token will no more be refreshed." 83 | clearInterval @timer if @timer? 84 | 85 | 86 | 87 | module.exports = LoopbackServer 88 | -------------------------------------------------------------------------------- /src/lib/model-config-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | ConfigJSONGenerator = require './config-json-generator' 5 | 6 | class ModelConfigGenerator extends ConfigJSONGenerator 7 | 8 | defaultConfigsPath: normalize "#{__dirname}/../../default-values" 9 | 10 | configNames: [ 'model-config' ] 11 | 12 | 13 | ###* 14 | @constructor 15 | ### 16 | constructor: (entityNames = []) -> 17 | 18 | @customConfigObj = 19 | 'model-config': @getConfigByEntityNames(entityNames) 20 | 21 | 22 | generate: -> 23 | generated = super() 24 | return generated['model-config'] 25 | 26 | 27 | ###* 28 | get config object by entity names 29 | @private 30 | @param {Array(String)} entityNames 31 | ### 32 | getConfigByEntityNames: (entityNames = []) -> 33 | 34 | config = {} 35 | for entityName in entityNames 36 | config[entityName] = 37 | dataSource: 'db' 38 | public: true 39 | 40 | return config 41 | 42 | module.exports = ModelConfigGenerator 43 | -------------------------------------------------------------------------------- /src/lib/model-definition.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | AclGenerator = require './acl-generator' 4 | 5 | ###* 6 | @class ModelDefinition 7 | ### 8 | class ModelDefinition 9 | 10 | constructor: (@modelName, @customDefinition = {}, @relationDefinitions = {}) -> 11 | 12 | @definition = @getDefaultDefinition() 13 | @definition[k] = @customDefinition[k] for k, v of @customDefinition 14 | 15 | @setACL() 16 | 17 | ###* 18 | get model name 19 | 20 | @method getName 21 | @public 22 | @return {String} modelName 23 | ### 24 | getName: -> 25 | @Entity.getName() 26 | 27 | 28 | ###* 29 | get stringified JSON contents about the model 30 | 31 | @method toStringifiedJSON 32 | @public 33 | @return {String} stringifiedJSON 34 | ### 35 | toStringifiedJSON: -> 36 | JSON.stringify @toJSON(), null, 2 37 | 38 | 39 | ###* 40 | get definition of the model 41 | 42 | @method toJSON 43 | @private 44 | @return {Object} definition 45 | ### 46 | toJSON: -> 47 | 48 | return @definition 49 | 50 | 51 | ###* 52 | is model extend User? 53 | 54 | @private 55 | @return {Boolean} 56 | ### 57 | isUser: -> 58 | @definition.base is 'User' 59 | 60 | 61 | ###* 62 | set ACL to definition by aclType 63 | 64 | ### 65 | setACL: -> 66 | @aclType = @definition.aclType 67 | delete @definition.aclType 68 | 69 | if not @aclType and Array.isArray @definition.acls 70 | @aclType = 'custom' 71 | return 72 | 73 | @aclType ?= 'admin' 74 | @definition.acls = new AclGenerator(@aclType, @isUser(), @relationDefinitions).generate() 75 | 76 | 77 | ###* 78 | get default definition object 79 | 80 | @private 81 | ### 82 | getDefaultDefinition: -> 83 | name : @modelName 84 | plural : @modelName 85 | base : "PersistedModel" 86 | idInjection : true 87 | properties : {} 88 | validations : [] 89 | relations : {} 90 | 91 | 92 | 93 | module.exports = ModelDefinition 94 | -------------------------------------------------------------------------------- /src/lib/models-generator.coffee: -------------------------------------------------------------------------------- 1 | 2 | { normalize } = require 'path' 3 | 4 | fs = require 'fs-extra' 5 | 6 | ModelDefinition = require './model-definition' 7 | ModelConfigGenerator = require './model-config-generator' 8 | 9 | class ModelsGenerator 10 | 11 | destinationDir : normalize "#{__dirname}/../../loopback/common/models" 12 | builtinDir : normalize "#{__dirname}/../../default-values/models" 13 | 14 | ###* 15 | @param {Object} customModelDefinitions model definition data, compatible with loopback's model-config.json and aclType 16 | ### 17 | constructor: (customModelDefinitions) -> 18 | 19 | @definitions = @createModelDefinitions(customModelDefinitions) 20 | 21 | entityNames = Object.keys @definitions 22 | @modelConfigGenerator = new ModelConfigGenerator(entityNames) 23 | 24 | 25 | ###* 26 | generate model-config.json and model definition files 27 | 28 | @method generate 29 | @public 30 | @return {Object} generatedInfo 31 | ### 32 | generate: -> 33 | 34 | modelConfig = @generateModelConfig() 35 | modelNames = @generateDefinitionFiles() 36 | 37 | config: modelConfig 38 | names : modelNames 39 | 40 | 41 | ###* 42 | generate JSON files with empty js files into common/models 43 | 44 | @method generateDefinitionFiles 45 | @return {Array} generatedModelNames 46 | ### 47 | generateDefinitionFiles: -> 48 | 49 | fs.mkdirsSync @destinationDir 50 | 51 | modelNames = for name, definition of @definitions 52 | @generateJSONandJS(name, definition) 53 | 54 | builtinModelNames = @generateBuiltinModels() 55 | 56 | return modelNames.concat builtinModelNames 57 | 58 | 59 | ###* 60 | reset 61 | 62 | @method reset 63 | @public 64 | @return 65 | ### 66 | reset: -> 67 | if fs.existsSync @destinationDir 68 | fs.removeSync @destinationDir 69 | 70 | @modelConfigGenerator.reset() 71 | 72 | 73 | ###* 74 | 75 | @method generateBuiltinModels 76 | @private 77 | ### 78 | generateBuiltinModels: -> 79 | 80 | for filename in fs.readdirSync @builtinDir 81 | [modelName, ext] = filename.split('.') 82 | definition = require @builtinDir + '/' + filename 83 | @generateJSONandJS(modelName, definition) 84 | 85 | 86 | ###* 87 | @method generateModelConfig 88 | @private 89 | ### 90 | generateModelConfig: -> 91 | 92 | @modelConfigGenerator.generate() 93 | 94 | 95 | 96 | ###* 97 | generate JSON file and JS file of modelName 98 | 99 | @private 100 | @reurn {String} modelName 101 | ### 102 | generateJSONandJS: (modelName, modelDefinition) -> 103 | 104 | jsonFilePath = normalize "#{@destinationDir}/#{modelName}.json" 105 | jsFilePath = normalize "#{@destinationDir}/#{modelName}.js" 106 | 107 | if modelDefinition not instanceof ModelDefinition 108 | jsonContent = JSON.stringify(modelDefinition, null, 2) 109 | fs.writeFileSync(jsonFilePath, jsonContent) 110 | fs.writeFileSync(jsFilePath, @getEmptyJSContent()) 111 | return modelName 112 | 113 | jsonContent = modelDefinition.toStringifiedJSON() 114 | fs.writeFileSync(jsonFilePath, jsonContent) 115 | fs.writeFileSync(jsFilePath, @getJSContent(modelDefinition.definition.validations)) 116 | 117 | return modelName 118 | 119 | 120 | ###* 121 | get empty js content 122 | 123 | @private 124 | ### 125 | getEmptyJSContent: -> 126 | 'module.exports = function(Model) {};' 127 | 128 | ###* 129 | get js content 130 | 131 | @private 132 | ### 133 | getJSContent: (validations) -> 134 | if not validations 135 | return @getEmptyJSContent() 136 | 137 | validateMethods = [] 138 | for validation in validations 139 | for prop, rules of validation 140 | if rules.required 141 | validateMethods.push(" Model.validatesPresenceOf('#{prop}');") 142 | if rules.pattern 143 | validateMethods.push(" Model.validatesFormatOf('#{prop}', { with: /#{rules.pattern}/ });") 144 | if rules.min 145 | validateMethods.push(" Model.validatesLengthOf('#{prop}', { min: #{rules.min} });") 146 | if rules.max 147 | validateMethods.push(" Model.validatesLengthOf('#{prop}', { max: #{rules.max} });") 148 | 149 | head = 'module.exports = function(Model) {\n' 150 | foot = '\n};\n' 151 | head + validateMethods.join('\n') + foot 152 | 153 | ###* 154 | get RelationDefinition 155 | 156 | @private 157 | ### 158 | getRelationDefinitions: (customModelDefinition, customModelDefinitions) -> 159 | 160 | definitions = {} 161 | 162 | for relationName, relationDefinition of customModelDefinition.relations 163 | 164 | continue unless customModelDefinitions[relationName] 165 | 166 | switch relationDefinition.type 167 | when 'hasMany' 168 | definitions[relationName] = 169 | type: relationDefinition.type 170 | aclType: customModelDefinitions[relationName].aclType 171 | 172 | return definitions 173 | 174 | ###* 175 | create ModelDefinition instances 176 | 177 | @private 178 | ### 179 | createModelDefinitions: (customModelDefinitions) -> 180 | 181 | definitions = {} 182 | 183 | for modelName, customModelDefinition of customModelDefinitions 184 | 185 | if customModelDefinition.relations? 186 | relationDefinitions = @getRelationDefinitions(customModelDefinition, customModelDefinitions) 187 | 188 | definitions[modelName] = new ModelDefinition(modelName, customModelDefinition, relationDefinitions) 189 | 190 | return definitions 191 | 192 | 193 | module.exports = ModelsGenerator 194 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | 2 | LoopbackInfo = require './lib/loopback-info' 3 | LoopbackServer = require './lib/loopback-server' 4 | 5 | ConfigJSONGenerator = require './lib/config-json-generator' 6 | ModelsGenerator = require './lib/models-generator' 7 | BuildInfoGenerator = require './lib/build-info-generator' 8 | CustomConfigs = require './lib/custom-configs' 9 | LoopbackBootGenerator = require './lib/loopback-boot-generator' 10 | 11 | ###* 12 | entry point 13 | 14 | @class Main 15 | ### 16 | class Main 17 | 18 | ###* 19 | entry point. 20 | run loopback with model definitions, config 21 | 22 | @method run 23 | @public 24 | @static 25 | @param {Object} loopbackDefinitions 26 | @param {Object|String} [config] config object or config directory containing config info 27 | @param {Boolean} [options.reset] reset previously-generated settings before generation 28 | @param {String} [options.env] set environment (production|development|...) 29 | @param {String} [options.participantToken] token for participant user 30 | @param {Object} [options.admin] options for admin token manager 31 | @param {Function|Array(String)} [options.admin.fetch] function to return admin tokens (or promise of it). When string[] is given, these value are used for the admin access token. 32 | @param {String} [options.admin.email=loopback-with-admin@example.com] email address for admin user 33 | @param {String} [options.admin.id=loopback-with-admin-user-id] id of admin user 34 | @param {String} [options.admin.password=admin-user-password] password of admin user 35 | @param {Number} [options.admin.intervalHours] IntervalHours to fetch new admin token. 36 | return {Promise(LoopbackInfo)} 37 | ### 38 | @run: (loopbackDefinitions, config, options = {}) -> 39 | 40 | main = new @(loopbackDefinitions, config, options.env) 41 | 42 | main.reset() unless options.reset is false 43 | 44 | generated = main.generate() 45 | 46 | if (options.adminToken) 47 | console.error 'LoopbackWithAdmin.run(): options.adminToken is deprecated. Use options.admin instead.' 48 | 49 | adminOptions = options.admin or options.adminToken # adminToken is for backward compatibility 50 | 51 | @launchLoopback(adminOptions, options.participantToken).then (server) => 52 | return new LoopbackInfo(server, generated) 53 | 54 | 55 | ###* 56 | @constructor 57 | @private 58 | ### 59 | constructor: (loopbackDefinitions, configs, @env) -> 60 | 61 | if loopbackDefinitions.models? 62 | modelDefinitions = loopbackDefinitions.models 63 | { customRoles } = loopbackDefinitions 64 | else 65 | modelDefinitions = loopbackDefinitions 66 | customRoles = null 67 | 68 | @env ?= process.env.NODE_ENV or 'development' 69 | 70 | customConfigs = new CustomConfigs(configs, @env) 71 | configObj = customConfigs.toObject() 72 | 73 | @configJSONGenerator = new ConfigJSONGenerator(configObj, @env) 74 | @modelsGenerator = new ModelsGenerator(modelDefinitions) 75 | @bootGenerator = new LoopbackBootGenerator(customRoles: customRoles) 76 | @buildInfoGenerator = new BuildInfoGenerator(modelDefinitions, configObj, @env) 77 | 78 | 79 | ###* 80 | @private 81 | ### 82 | generate: -> 83 | config : @configJSONGenerator.generate() 84 | models : @modelsGenerator.generate() 85 | buildInfo : @buildInfoGenerator.generate() 86 | bootInfo : @bootGenerator.generate() 87 | 88 | ###* 89 | @private 90 | ### 91 | reset: -> 92 | @configJSONGenerator.reset() 93 | @modelsGenerator.reset() 94 | @buildInfoGenerator.reset() 95 | @bootGenerator.reset() 96 | 97 | 98 | 99 | ###* 100 | run loopback 101 | 102 | @private 103 | ### 104 | @launchLoopback: (adminOptions, participantToken) -> 105 | 106 | server = new LoopbackServer() 107 | server.launch(adminOptions, participantToken).then => server 108 | 109 | module.exports = Main 110 | -------------------------------------------------------------------------------- /src/server/admin-token-manager.coffee: -------------------------------------------------------------------------------- 1 | ____ = require('debug')('loopback-with-admin:admin-token-manager') 2 | 3 | DEFAULT_ADMIN_USER = 4 | email: 'loopback-with-admin@example.com' 5 | id: 'loopback-with-admin-user-id' 6 | password: 'admin-user-password' # No worry, noone can login through REST API. 7 | 8 | ONE_YEAR = 60 * 60 * 24 * 365 9 | 10 | DEFAULT_TOKEN = 'loopback-with-admin-token' 11 | 12 | promisify = (fn) -> 13 | new Promise (y, n) => 14 | cb = (e, o) => if e? then n(e) else y(o) 15 | fn(cb) 16 | 17 | 18 | ###* 19 | Admin token manager 20 | 21 | @class AdminTokenManager 22 | ### 23 | class AdminTokenManager 24 | 25 | ###* 26 | @param {Function|Array(String)} [options.fetch] function to return admin tokens (or promise of it). When string[] is given, these value are used for the admin access token. 27 | @param {String} [options.email=loopback-with-admin@example.com] email address for admin user 28 | @param {String} [options.id=loopback-with-admin-user-id] id of admin user 29 | @param {String} [options.password=admin-user-password] password of admin user 30 | ### 31 | constructor: (options = {}) -> 32 | 33 | { fetch, email, id, password } = options 34 | 35 | @fetch = @constructor.createFetchFunction(fetch) 36 | 37 | @adminUser = 38 | email: email or DEFAULT_ADMIN_USER.email 39 | id: id or DEFAULT_ADMIN_USER.id 40 | password: password or DEFAULT_ADMIN_USER.password 41 | 42 | @tokensById = {} 43 | 44 | 45 | 46 | ###* 47 | Set fetched tokens as admin tokens. 48 | 49 | @public 50 | @method init 51 | @param {Object} models app.models in LoopBack 52 | @return {Promise} 53 | ### 54 | init: (@models) -> 55 | 56 | @createAdminUser() 57 | 58 | .then => 59 | @createAdminRole() 60 | 61 | .then => 62 | @fetch() 63 | 64 | .then (tokenStrs) => 65 | 66 | if not @validTokenStrs(tokenStrs) 67 | throw @invalidTokenError(tokenStrs) 68 | 69 | @updateTokens(tokenStrs) 70 | 71 | 72 | 73 | ###* 74 | Refresh admin tokens. 75 | 76 | @public 77 | @method refreshTokens 78 | @return {Promise} 79 | ### 80 | refreshTokens: -> 81 | 82 | @fetch().then (tokenStrs) => 83 | 84 | if not @validTokenStrs(tokenStrs) 85 | 86 | console.error(""" 87 | AdminTokenManager: Fetched tokens are not valid! 88 | 89 | Results: #{tokenStrs} 90 | 91 | """) 92 | return Promise.resolve(false) 93 | 94 | @updateTokens(tokenStrs) 95 | 96 | 97 | ###* 98 | Get current tokens 99 | @public 100 | @method getCurrentTokens 101 | @return {Array(String)} 102 | ### 103 | getCurrentTokens: -> 104 | Object.keys @tokensById 105 | 106 | 107 | ###* 108 | Save new tokens and destroy old tokens. 109 | @private 110 | ### 111 | updateTokens: (tokenStrs) -> 112 | 113 | tokens = tokenStrs.map (tokenStr) => new AdminToken(tokenStr, @adminUser.id) 114 | 115 | Promise.all(tokens.map (token) => @setNew token).then => 116 | 117 | promises = [] 118 | 119 | for tokenStr of @tokensById when tokenStr not in tokenStrs 120 | promises.push @destroy(tokenStr) 121 | 122 | Promise.all promises 123 | 124 | .then => 125 | ____("tokens: #{Object.keys(@tokensById).join(',')}") 126 | 127 | 128 | ###* 129 | set new token 130 | @private 131 | ### 132 | setNew: (token) -> 133 | 134 | { AccessToken } = @models 135 | 136 | @findById(token.id).then (foundToken) => 137 | 138 | if foundToken? 139 | ____("token: #{token.id} already exists.") 140 | 141 | if foundToken.userId isnt @adminUser.id 142 | console.error """ 143 | AdminTokenManager: The token `#{token.id}` is already exist for non-admin user. Skip creating. 144 | """ 145 | console.error() 146 | 147 | return false 148 | 149 | ____("saving token: #{token.id}") 150 | promisify (cb) => 151 | AccessToken.create token, cb 152 | 153 | .then => true 154 | 155 | .then (tokenIsSavedNow) => 156 | @tokensById[token.id] = token 157 | 158 | 159 | 160 | ###* 161 | Destroy the token 162 | @private 163 | ### 164 | destroy: (tokenStr) -> 165 | 166 | @findById(tokenStr).then (foundToken) => 167 | # check if the token to be deleted is admin token 168 | if foundToken.userId isnt @adminUser.id 169 | console.error """ 170 | AdminTokenManager: The token `#{token.id}` is not the admin token. Skip destroying. 171 | """ 172 | return false 173 | 174 | { AccessToken } = @models 175 | 176 | promisify (cb) => 177 | AccessToken.destroyById tokenStr, cb 178 | 179 | .then => 180 | delete @tokensById[tokenStr] 181 | 182 | 183 | ###* 184 | Find AccessToken model by tokenStr 185 | @private 186 | ### 187 | findById: (tokenStr) -> 188 | 189 | { AccessToken } = @models 190 | 191 | promisify (cb) => 192 | AccessToken.findById tokenStr, cb 193 | 194 | 195 | 196 | ###* 197 | Create admin user, called once in 'init' function. 198 | @private 199 | ### 200 | createAdminUser: -> 201 | ____("creating admin user. id: #{@adminUser.id}") 202 | { User } = @models 203 | 204 | promisify (cb) => 205 | User.create @adminUser, cb 206 | 207 | 208 | ###* 209 | Create admin role, called once in 'init' function. 210 | @private 211 | ### 212 | createAdminRole: -> 213 | 214 | ____("creating admin role.") 215 | { Role, RoleMapping } = @models 216 | 217 | promisify (cb) => 218 | Role.create name: 'admin', cb 219 | 220 | .then (role) => 221 | principal = 222 | principalType: RoleMapping.USER 223 | principalId: @adminUser.id 224 | 225 | promisify (cb) => 226 | role.principals.create principal, cb 227 | 228 | 229 | ###* 230 | Check the fetched results are valid 231 | @private 232 | ### 233 | validTokenStrs: (tokenStrs) -> 234 | 235 | Array.isArray(tokenStrs) and tokenStrs.length > 0 and tokenStrs.every (v) -> typeof v is 'string' 236 | 237 | 238 | 239 | ###* 240 | Create an error to indicate the tokenStrs are invalid 241 | @private 242 | ### 243 | invalidTokenError: (tokenStrs) -> 244 | 245 | new Error """ 246 | AdminTokenManager could not fetch valid access tokens. 247 | Result: '#{tokenStrs}' 248 | Check if the valid function is passed to the 3rd arugment of run() method. 249 | 250 | var fn = function() { 251 | return Promise.resolve(['token1', 'token2', 'token3']) 252 | }; 253 | 254 | require('loopback-with-admin').run(models, config, { admin: {fetch: fn} }) 255 | """ 256 | 257 | 258 | 259 | ###* 260 | Create valid fetch function 261 | @private 262 | @static 263 | ### 264 | @createFetchFunction: (fetch) -> 265 | 266 | if not fetch? 267 | return => Promise.resolve([DEFAULT_TOKEN]) 268 | 269 | if typeof fetch is 'string' 270 | return => Promise.resolve([fetch]) 271 | 272 | if Array.isArray fetch 273 | return => Promise.resolve(fetch.slice()) 274 | 275 | if typeof fetch isnt 'function' 276 | return => Promise.resolve([DEFAULT_TOKEN]) 277 | 278 | # if typeof fetch is 'function' 279 | return => 280 | Promise.resolve(fetch()).then (results) => 281 | if typeof results is 'string' 282 | return [results] 283 | 284 | if Array.isArray results 285 | return results 286 | 287 | return [] # will throw error in init() 288 | 289 | 290 | ###* 291 | Admin token 292 | 293 | @class AdminToken 294 | @private 295 | ### 296 | class AdminToken 297 | 298 | constructor: (@id, @userId) -> 299 | @ttl = ONE_YEAR 300 | @isAdmin = true 301 | 302 | 303 | 304 | 305 | module.exports = AdminTokenManager 306 | -------------------------------------------------------------------------------- /src/server/boot/notification.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (app, cb) -> 3 | 4 | Installation = app.models.installation 5 | 6 | Installation.observe 'before save', (ctx, next) -> 7 | if ctx.instance 8 | ctx.instance.appId = 'loopback-with-admin' 9 | next() 10 | 11 | registerApp(app, cb) 12 | 13 | 14 | ###* 15 | registers an application instance for push notification service 16 | 17 | @method registerApp 18 | ### 19 | registerApp = (app, cb) -> 20 | Application = app.models.application 21 | 22 | config = require('../push-credentials') 23 | buildInfo = require('../build-info') 24 | 25 | Application.observe 'before save', (ctx, next) -> 26 | ctx.instance.id = 'loopback-with-admin' 27 | next() 28 | 29 | Application.register( 30 | 'CureApp, Inc.' 31 | 'loopback-with-admin' 32 | { 33 | descriptions: '' 34 | pushSettings: 35 | apns: 36 | production: buildInfo.env is 'production' 37 | certData: config.apnsCertData 38 | keyData: config.apnsKeyData 39 | 40 | feeedbackOptions: 41 | batchFeedback: true 42 | interval: 300 43 | 44 | gcm: 45 | serverApiKey: config.gcmServerApiKey 46 | 47 | } 48 | (err, savedApp) -> 49 | console.log err if err 50 | cb() 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /src/server/participant-token-setter.coffee: -------------------------------------------------------------------------------- 1 | ____ = require('debug')('loopback-with-admin:participant-token-setter') 2 | 3 | PARTICIPANT_USER = 4 | email: 'loopback-with-participant@example.com' 5 | id: 'loopback-with-admin-participant' 6 | password: 'participant-user-password' # No worry, noone can login through REST API. 7 | 8 | HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100 9 | 10 | DEFAULT_TOKEN = 'loopback-with-admin-participant' 11 | 12 | promisify = (fn) -> 13 | new Promise (y, n) => 14 | cb = (e, o) => if e? then n(e) else y(o) 15 | fn(cb) 16 | 17 | 18 | ###* 19 | Participant token setter 20 | 21 | @class ParticipantTokenSetter 22 | ### 23 | class ParticipantTokenSetter 24 | 25 | ###* 26 | @param {String} token participant token 27 | ### 28 | constructor: (@token = DEFAULT_TOKEN) -> 29 | 30 | 31 | set: (@models) -> 32 | 33 | @createUser() 34 | .then => 35 | @createRole() 36 | .then => 37 | @setToken(@token) 38 | 39 | ###* 40 | Create participant user 41 | @private 42 | ### 43 | createUser: -> 44 | ____("creating participant user. id: #{PARTICIPANT_USER.id}") 45 | { User } = @models 46 | 47 | promisify (cb) => 48 | User.create PARTICIPANT_USER, cb 49 | 50 | 51 | ###* 52 | Create participant role 53 | @private 54 | ### 55 | createRole: -> 56 | 57 | ____("creating participant role.") 58 | { Role, RoleMapping } = @models 59 | 60 | promisify (cb) => 61 | Role.create name: 'participant', cb 62 | 63 | .then (role) => 64 | principal = 65 | principalType: RoleMapping.USER 66 | principalId: PARTICIPANT_USER.id 67 | 68 | promisify (cb) => 69 | role.principals.create principal, cb 70 | 71 | 72 | ###* 73 | set new token 74 | @private 75 | ### 76 | setToken: (token) -> 77 | 78 | { AccessToken } = @models 79 | 80 | @findById(token).then (foundToken) => 81 | 82 | if foundToken? 83 | ____("token: #{token} already exists.") 84 | 85 | if foundToken.userId isnt PARTICIPANT_USER.id 86 | console.error """ 87 | ParticipantTokenSetter: The token `#{token}` is already exist for non-participant user. Skip creating. 88 | """ 89 | console.error() 90 | 91 | return false 92 | 93 | ____("saving token: #{token}") 94 | promisify (cb) => 95 | AccessToken.create { id: token, userId: PARTICIPANT_USER.id, ttl: HUNDRED_YEARS }, cb 96 | 97 | .then => true 98 | 99 | 100 | ###* 101 | Find AccessToken model by tokenStr 102 | @private 103 | ### 104 | findById: (tokenStr) -> 105 | 106 | { AccessToken } = @models 107 | 108 | promisify (cb) => 109 | AccessToken.findById tokenStr, cb 110 | 111 | 112 | module.exports = ParticipantTokenSetter 113 | --------------------------------------------------------------------------------