├── .bowerrc ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .github ├── stale.yml └── workflows │ └── codeql.yml ├── .gitignore ├── .sailsrc ├── .stylelintrc ├── Dockerfile ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── api ├── controllers │ ├── .gitkeep │ ├── AssetController.js │ ├── AuthController.js │ ├── ChannelController.js │ ├── FlavorController.js │ └── VersionController.js ├── models │ ├── .gitkeep │ ├── Asset.js │ ├── Channel.js │ ├── Flavor.js │ └── Version.js ├── policies │ ├── authToken.js │ └── noCache.js ├── responses │ ├── badRequest.js │ ├── forbidden.js │ ├── notFound.js │ ├── ok.js │ └── serverError.js └── services │ ├── .gitkeep │ ├── AssetService.js │ ├── AuthService.js │ ├── AuthToken.js │ ├── ChannelService.js │ ├── FlavorService.js │ ├── PlatformService.js │ ├── UtilityService.js │ ├── VersionService.js │ └── WindowsReleaseService.js ├── app.js ├── assets ├── favicon.ico ├── images │ ├── .gitkeep │ ├── logo.svg │ └── menu-icon.svg ├── js │ ├── admin │ │ ├── add-flavor-modal │ │ │ ├── add-flavor-modal-controller.js │ │ │ └── add-flavor-modal.pug │ │ ├── add-version-asset-modal │ │ │ ├── add-version-asset-modal-controller.js │ │ │ └── add-version-asset-modal.pug │ │ ├── add-version-modal │ │ │ ├── add-version-modal-controller.js │ │ │ └── add-version-modal.pug │ │ ├── admin.js │ │ ├── edit-version-asset-modal │ │ │ ├── edit-version-asset-modal-controller.js │ │ │ └── edit-version-asset-modal.pug │ │ ├── edit-version-modal │ │ │ ├── edit-version-modal-controller.js │ │ │ └── edit-version-modal.pug │ │ └── version-table │ │ │ ├── version-table-controller.js │ │ │ └── version-table.pug │ ├── core │ │ ├── auth │ │ │ ├── auth-service.js │ │ │ ├── auth.js │ │ │ ├── login │ │ │ │ ├── login-controller.js │ │ │ │ └── login.pug │ │ │ └── logout │ │ │ │ ├── logout-controller.js │ │ │ │ └── logout.pug │ │ ├── core.js │ │ ├── data │ │ │ ├── data-service.js │ │ │ └── data.js │ │ ├── dependencies │ │ │ └── dependencies.js │ │ └── nav-controller.js │ ├── download │ │ ├── download-controller.js │ │ └── download.pug │ ├── home │ │ ├── home-controller.js │ │ └── home.pug │ └── main.js ├── robots.txt ├── styles │ ├── _bootswatch.scss │ ├── _custom.scss │ ├── _variable_override.scss │ ├── _variables.scss │ └── importer.scss └── templates │ ├── .gitkeep │ └── auth-toolbar.pug ├── bower.json ├── config ├── blueprints.js ├── bootstrap.js ├── channels.js ├── csrf.js ├── datastores.js ├── docker.js ├── env │ ├── development.js │ └── production.js ├── files.js ├── flavors.js ├── globals.js ├── http.js ├── i18n.js ├── local.template ├── locales │ ├── _README.md │ ├── de.json │ ├── en.json │ ├── es.json │ └── fr.json ├── log.js ├── models.js ├── policies.js ├── routes.js ├── session.js ├── sockets.js └── views.js ├── database.json.template ├── docker-compose.yml ├── docs ├── README.md ├── api.md ├── assets.md ├── authentication.md ├── customization.md ├── database.md ├── deploy.md ├── docker.md ├── faq.md ├── update-osx.md ├── update-windows.md ├── urls.md └── using-it.md ├── migrations ├── 20190930000000-asset-migration.js ├── 20190930000000-availability-migration.js └── 20190930000000-flavor-migration.js ├── package-lock.json ├── package.json ├── scripts └── wait.sh ├── tasks ├── README.md ├── config │ ├── clean.js │ ├── coffee.js │ ├── concat.js │ ├── copy.js │ ├── cssmin.js │ ├── jst.js │ ├── less.js │ ├── pug.js │ ├── sails-linker.js │ ├── sass.js │ ├── sync.js │ ├── uglify.js │ ├── watch.js │ └── wiredep.js ├── pipeline.js └── register │ ├── build.js │ ├── buildProd.js │ ├── compileAssets.js │ ├── default.js │ ├── linkAssets.js │ ├── linkAssetsBuild.js │ ├── linkAssetsBuildProd.js │ ├── prod.js │ └── syncAssets.js └── views ├── 403.pug ├── 404.pug ├── 500.pug └── homepage.pug /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "assets/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | config/local.js 2 | database.json 3 | node_modules 4 | assets/bower_components 5 | .tmp 6 | .sass-cache 7 | dump.rdb 8 | lib-cov 9 | *.seed 10 | *.log 11 | *.out 12 | *.pid 13 | npm-debug.log 14 | *~ 15 | *# 16 | .DS_STORE 17 | .netbeans 18 | nbproject 19 | .idea 20 | .node_history 21 | Dockerfile 22 | docker-compose.json 23 | releases/ 24 | postgresql/ 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true, 6 | 'jquery': true, 7 | 'node': true 8 | }, 9 | 'extends': [ 10 | 'eslint:recommended' 11 | ], 12 | 'globals': { 13 | 'angular': 'readonly', 14 | 'sails': 'readonly', 15 | 16 | 'Asset': 'readonly', 17 | 'Channel': 'readonly', 18 | 'Flavor': 'readonly', 19 | 'Version': 'readonly', 20 | 21 | 'AssetService': 'readonly', 22 | 'AuthService': 'readonly', 23 | 'AuthToken': 'readonly', 24 | 'ChannelService': 'readonly', 25 | 'FlavorService': 'readonly', 26 | 'PlatformService': 'readonly', 27 | 'UtilityService': 'readonly', 28 | 'VersionService': 'readonly', 29 | 'WindowsReleaseService': 'readonly' 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - feature_request 10 | - documentation 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "38 3 * * 3" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | 20 | 21 | 22 | 23 | ################################################ 24 | # Local Configuration 25 | # 26 | # Explicitly ignore files which contain: 27 | # 28 | # 1. Sensitive information you'd rather not push to 29 | # your git repository. 30 | # e.g., your personal API keys or passwords. 31 | # 32 | # 2. Environment-specific configuration 33 | # Basically, anything that would be annoying 34 | # to have to change every time you do a 35 | # `git pull` 36 | # e.g., your local development database, or 37 | # the S3 bucket you're using for file uploads 38 | # development. 39 | # 40 | ################################################ 41 | 42 | config/local.js 43 | database.json 44 | 45 | 46 | 47 | 48 | 49 | ################################################ 50 | # Dependencies 51 | # 52 | # When releasing a production app, you may 53 | # consider including your node_modules and 54 | # bower_components directory in your git repo, 55 | # but during development, its best to exclude it, 56 | # since different developers may be working on 57 | # different kernels, where dependencies would 58 | # need to be recompiled anyway. 59 | # 60 | # More on that here about node_modules dir: 61 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 62 | # (credit Mikeal Rogers, @mikeal) 63 | # 64 | # About bower_components dir, you can see this: 65 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 66 | # (credit Addy Osmani, @addyosmani) 67 | # 68 | ################################################ 69 | 70 | node_modules 71 | bower_components 72 | 73 | 74 | 75 | 76 | ################################################ 77 | # Sails.js / Waterline / Grunt 78 | # 79 | # Files generated by Sails and Grunt, or related 80 | # tasks and adapters. 81 | ################################################ 82 | .tmp 83 | .sass-cache 84 | dump.rdb 85 | 86 | 87 | 88 | 89 | 90 | ################################################ 91 | # Node.js / NPM 92 | # 93 | # Common files generated by Node, NPM, and the 94 | # related ecosystem. 95 | ################################################ 96 | lib-cov 97 | *.seed 98 | *.log 99 | *.out 100 | *.pid 101 | npm-debug.log 102 | 103 | 104 | 105 | 106 | 107 | ################################################ 108 | # Miscellaneous 109 | # 110 | # Common files generated by text editors, 111 | # operating systems, file systems, etc. 112 | ################################################ 113 | 114 | *~ 115 | *# 116 | .DS_STORE 117 | .netbeans 118 | nbproject 119 | .idea 120 | .node_history 121 | releases 122 | -------------------------------------------------------------------------------- /.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "modules": {} 4 | } 5 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "at-rule-no-unknown": null, 5 | "no-descending-specificity": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:19 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/electron-release-server 5 | 6 | # Install app dependencies 7 | COPY package.json .bowerrc bower.json /usr/src/electron-release-server/ 8 | RUN npm install \ 9 | && ./node_modules/.bin/bower install --allow-root \ 10 | && npm cache clean --force \ 11 | && npm prune --production 12 | 13 | # Bundle app source 14 | COPY . /usr/src/electron-release-server 15 | 16 | COPY config/docker.js config/local.js 17 | 18 | EXPOSE 80 19 | 20 | CMD [ "npm", "start" ] 21 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile 3 | * 4 | * This Node script is executed when you run `grunt` or `sails lift`. 5 | * It's purpose is to load the Grunt tasks in your project's `tasks` 6 | * folder, and allow you to add and remove tasks as you see fit. 7 | * For more information on how this works, check out the `README.md` 8 | * file that was generated in your `tasks` folder. 9 | * 10 | * WARNING: 11 | * Unless you know what you're doing, you shouldn't change this file. 12 | * Check out the `tasks` directory instead. 13 | */ 14 | 15 | module.exports = function(grunt) { 16 | 17 | var loadGruntTasks = require('sails-hook-grunt/accessible/load-grunt-tasks'); 18 | 19 | // Load Grunt task configurations (from `tasks/config/`) and Grunt 20 | // task registrations (from `tasks/register/`). 21 | loadGruntTasks(__dirname, grunt); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Arek Sredzki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Release Server 2 | [](https://github.com/ArekSredzki/electron-release-server/stargazers) 3 | [](https://github.com/ArekSredzki/electron-release-server/network) 4 | [](https://gitter.im/ArekSredzki/electron-release-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | >A node web server which serves & manages releases of your [Electron](http://electron.atom.io) App, and is fully compatible with [Squirrel](https://github.com/Squirrel) Auto-updater (which is built into Electron). 6 | 7 | [](https://youtu.be/lvT7rfB01iA) 8 | 9 | _Note: Despite being advertised as a release server for Electron applications, it would work for **any application using Squirrel**._ 10 | 11 | If you host your project on your Github **and** do not need a UI for your app, then [Nuts](https://github.com/GitbookIO/nuts) is probably what you're looking for. Otherwise, you're in the same boat as I was, and you've found the right place! 12 | 13 | ## Advisory Notices 14 | **IMPORTANT:** 15 | - Version `2.0.0` updates many packages, most importantly to SailsJS 1.x.x. At a bare minimum, you must rename `connections` to `datastores` in your `config/local.js` file. You may need to make further changes depending on how significantly you have customized the project. 16 | - The release of Angular `1.6.0` has broken all `electron-release-server` versions prior to `1.4.2`. Please use the instructions under the `Maintenance` heading below to update your fork! Sorry for the inconvenience. 17 | - Since release `1.5.0` several models have changed to accommodate new features. Please use the instructions under [Migration](docs/database.md#migration) to update your database! Sorry for the inconvenience. 18 | 19 | ## Features 20 | - :sparkles: Docker :whale: support (thanks to EvgeneOskin)! 21 | - :sparkles: Awesome release management interface powered by [AngularJS](https://angularjs.org) 22 | - Authenticates with LDAP, easy to modify to another authentication method if needed 23 | - :sparkles: Store assets on server disk, or Amazon S3 (with minor modifications) 24 | - Use pretty much any database for persistence, thanks to [Sails](http://sailsjs.org) & [Waterline](http://waterlinejs.org) 25 | - :sparkles: Code-less [app customization through env variables](docs/customization.md) 26 | - :sparkles: Simple but powerful download urls (**NOTE:** when no assets are uploaded, server returns `404` by default): 27 | - `/download/latest` 28 | - `/download/latest/:platform` 29 | - `/download/:version` 30 | - `/download/:version/:platform` 31 | - `/download/:version/:platform/:filename` 32 | - `/download/channel/:channel` 33 | - `/download/channel/:channel/:platform` 34 | - `/download/flavor/:flavor/latest` 35 | - `/download/flavor/:flavor/latest/:platform` 36 | - `/download/flavor/:flavor/:version` 37 | - `/download/flavor/:flavor/:version/:platform` 38 | - `/download/flavor/:flavor/:version/:platform/:filename` 39 | - `/download/flavor/:flavor/channel/:channel` 40 | - `/download/flavor/:flavor/channel/:channel/:platform` 41 | - :sparkles: Support pre-release channels (`beta`, `alpha`, ...) 42 | - :sparkles: Support multiple flavors of your app 43 | - :sparkles: Auto-updates with [Squirrel](https://github.com/Squirrel): 44 | - Update URLs provided: 45 | - `/update/:platform/:version[/:channel]` 46 | - `/update/flavor/:flavor/:platform/:version[/:channel]` 47 | - Mac uses `*.dmg` and `*.zip` 48 | - Windows uses `*.exe` and `*.nupkg` 49 | - :sparkles: Auto-updates with [NSIS differential updates for Windows](docs/update-windows.md#NSIS-differential-updates) 50 | - :sparkles: Serve the perfect type of assets: `.zip` for Squirrel.Mac, `.nupkg` for Squirrel.Windows, `.dmg` for Mac users, ... 51 | - :sparkles: Specify date of availability for releases 52 | - :sparkles: Release notes endpoint 53 | - `/notes/:version/:flavor?` 54 | 55 | **NOTE:** if you don't provide the appropriate type of file for Squirrel you won't be able to update your app since the update endpoint will not return a JSON. (`.zip` for Squirrel.Mac, `.nupkg` for Squirrel.Windows). 56 | 57 | ## Deploy it / Start it 58 | 59 | [Follow our guide to deploy Electron Release Server](docs/deploy.md). 60 | 61 | ## Auto-updater / Squirrel 62 | 63 | This server provides an endpoint for [Squirrel auto-updater](https://github.com/atom/electron/blob/master/docs/api/auto-updater.md), it supports both [OS X](docs/update-osx.md) and [Windows](docs/update-windows.md). 64 | 65 | ## Documentation 66 | [Check out the documentation](docs/) for more details. 67 | 68 | ## Building Releases 69 | I highly recommend using [electron-builder](https://github.com/loopline-systems/electron-builder) for packaging & releasing your applications. Once you have built your app with that, you can upload the artifacts for your users right away! 70 | 71 | ## Maintenance 72 | You should keep your fork up to date with the electron-release-server master. 73 | 74 | Doing so is simple, rebase your repo using the commands below. 75 | ```bash 76 | git remote add upstream https://github.com/ArekSredzki/electron-release-server.git 77 | git fetch upstream 78 | git rebase upstream/master 79 | ``` 80 | 81 | ## Credit 82 | This project has been built from Sails.js up by Arek Sredzki, with inspiration from [nuts](https://github.com/GitbookIO/nuts). 83 | 84 | ## License 85 | [MIT License](LICENSE.md) 86 | -------------------------------------------------------------------------------- /api/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/api/controllers/.gitkeep -------------------------------------------------------------------------------- /api/controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication Controller 3 | * 4 | */ 5 | 6 | var AuthController = { 7 | login: function(req, res) { 8 | AuthService.authenticate( 9 | req, 10 | sails.config.auth, 11 | function(err, user) { 12 | 13 | if (err) { 14 | return res.status(err.code || 401).send(err.message || 'Incorrect credentials'); 15 | } 16 | 17 | if (!user) { 18 | // If there is no error passed then we should have a user object 19 | return res.serverError('Could not retrieve user'); 20 | } 21 | 22 | return res.json({ 23 | user: user.username, 24 | token: AuthToken.issueToken({ 25 | sub: user.username 26 | }) 27 | }); 28 | }); 29 | } 30 | 31 | // Logout is handled client-side 32 | }; 33 | 34 | module.exports = AuthController; 35 | -------------------------------------------------------------------------------- /api/controllers/ChannelController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ChannelController 3 | * 4 | * @description :: Server-side logic for managing Channels 5 | * @help :: See http://sailsjs.org/#!/documentation/concepts/Controllers 6 | */ 7 | 8 | module.exports = { 9 | 10 | /** 11 | * Returns a list of channel names sorted by their priority 12 | * 13 | * ( GET /channels/sorted ) 14 | */ 15 | list: function (req, res) { 16 | res.send(ChannelService.availableChannels); 17 | }, 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /api/controllers/FlavorController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FlavorController 3 | * 4 | * @description :: Server-side logic for managing Flavors 5 | * @help :: See http://sailsjs.org/#!/documentation/concepts/Controllers 6 | */ 7 | 8 | const actionUtil = require('sails/lib/hooks/blueprints/actionUtil'); 9 | const Promise = require('bluebird'); 10 | 11 | const destroyAssetAndFile = (asset, req) => AssetService 12 | .destroy(asset, req) 13 | .then(AssetService.deleteFile(asset)) 14 | .then(() => sails.log.info('Destroyed asset:', asset)); 15 | 16 | const destroyAssetsAndFiles = (version, req) => version.assets 17 | .map(asset => destroyAssetAndFile(asset, req)); 18 | 19 | const destroyVersion = (version, req) => VersionService 20 | .destroy(version, req) 21 | .then(() => sails.log.info('Destroyed version:', version)); 22 | 23 | const destroyVersionAssetsAndFiles = (version, req) => Promise 24 | .all(destroyAssetsAndFiles(version, req)) 25 | .then(destroyVersion(version, req)); 26 | 27 | const destroyFlavor = (flavor, req) => FlavorService 28 | .destroy(flavor, req) 29 | .then(() => sails.log.info('Destroyed flavor:', flavor)); 30 | 31 | module.exports = { 32 | 33 | /** 34 | * Overloaded blueprint function 35 | * Changes: 36 | * - Delete all associated versions, assets & their files 37 | * @param {Object} req Incoming request object 38 | * @param {Object} res Outgoing response object 39 | */ 40 | destroy: (req, res) => { 41 | const pk = actionUtil.requirePk(req); 42 | 43 | if (pk === 'default') { 44 | res.serverError('Default flavor cannot be deleted.'); 45 | } else { 46 | Flavor 47 | .findOne(pk) 48 | .exec((err, flavor) => { 49 | if (err) { 50 | res.serverError(err); 51 | } else if (!flavor) { 52 | res.notFound('No flavor found with the specified `name`.'); 53 | } else { 54 | Version 55 | .find({ flavor: flavor.name }) 56 | .populate('assets') 57 | .exec((err, versions) => { 58 | if (err) { 59 | res.serverError(err); 60 | } else { 61 | Promise 62 | .map(versions, version => destroyVersionAssetsAndFiles(version, req)) 63 | .then(destroyFlavor(flavor, req)) 64 | .then(res.ok(flavor.name)) 65 | .error(res.negotiate); 66 | } 67 | }); 68 | } 69 | }); 70 | } 71 | } 72 | 73 | }; 74 | -------------------------------------------------------------------------------- /api/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/api/models/.gitkeep -------------------------------------------------------------------------------- /api/models/Asset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asset.js 3 | * 4 | * @description :: A software asset that can be used to install the app (ex. .exe, .dmg, .deb, etc.) 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | primaryKey: 'id', 11 | 12 | attributes: { 13 | 14 | id: { 15 | type: 'string', 16 | unique: true, 17 | required: true 18 | }, 19 | 20 | name: { 21 | type: 'string', 22 | required: true 23 | }, 24 | 25 | platform: { 26 | type: 'string', 27 | isIn: ['linux_32', 'linux_64', 'osx_64', 'osx_arm64', 'windows_32', 'windows_64'], 28 | required: true 29 | }, 30 | 31 | filetype: { 32 | type: 'string', 33 | required: true 34 | }, 35 | 36 | hash: { 37 | type: 'string' 38 | }, 39 | 40 | size: { 41 | type: 'number', 42 | required: true, 43 | columnType: 'integer' 44 | }, 45 | 46 | download_count: { 47 | type: 'number', 48 | defaultsTo: 0, 49 | columnType: 'integer' 50 | }, 51 | 52 | version: { 53 | model: 'version', 54 | required: true 55 | }, 56 | 57 | // File descriptor for the asset 58 | fd: { 59 | type: 'string', 60 | required: true 61 | } 62 | } 63 | 64 | }; 65 | -------------------------------------------------------------------------------- /api/models/Channel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Channel.js 3 | * 4 | * @description :: Various release channel (ex. stable & dev) 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | primaryKey: 'name', 11 | 12 | attributes: { 13 | name: { 14 | type: 'string', 15 | unique: true, 16 | required: true 17 | } 18 | }, 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /api/models/Flavor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flavor.js 3 | * 4 | * @description :: Represents a release flavor (ex. default) 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | primaryKey: 'name', 11 | 12 | attributes: { 13 | name: { 14 | type: 'string', 15 | unique: true, 16 | required: true 17 | } 18 | }, 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /api/models/Version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Version.js 3 | * 4 | * @description :: Represents a release version, which has a flavor, contains assets and is a member of a channel 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | 10 | primaryKey: 'id', 11 | 12 | attributes: { 13 | id: { 14 | type: 'string', 15 | unique: true, 16 | required: true 17 | }, 18 | 19 | name: { 20 | type: 'string', 21 | required: true 22 | }, 23 | 24 | assets: { 25 | collection: 'asset', 26 | via: 'version' 27 | }, 28 | 29 | channel: { 30 | model: 'channel', 31 | required: true 32 | }, 33 | 34 | availability: { 35 | type: 'string' 36 | }, 37 | 38 | flavor: { 39 | model: 'flavor', 40 | }, 41 | 42 | notes: { 43 | type: 'string' 44 | } 45 | }, 46 | 47 | beforeCreate: (version, proceed) => { 48 | const { name, flavor } = version; 49 | 50 | version.id = `${name}_${flavor}`; 51 | 52 | return proceed(); 53 | }, 54 | 55 | afterCreate: (version, proceed) => { 56 | const { availability, createdAt, id } = version; 57 | 58 | if (!availability || new Date(availability) < new Date(createdAt)) { 59 | return Version 60 | .update(id, { availability: createdAt }) 61 | .exec(proceed); 62 | } 63 | 64 | return proceed(); 65 | } 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /api/policies/authToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * authToken 3 | * 4 | * @module :: Policy 5 | * @description :: Ensure that the user is authenticated with an authToken 6 | * @docs :: http://sailsjs.org/#!/documentation/concepts/Policies 7 | * 8 | */ 9 | module.exports = function(req, res, next) { 10 | var token; 11 | 12 | if (req.headers && req.headers.authorization) { 13 | var parts = req.headers.authorization.split(' '); 14 | if (parts.length == 2) { 15 | var scheme = parts[0], 16 | credentials = parts[1]; 17 | 18 | if (/^Bearer$/i.test(scheme)) { 19 | token = credentials; 20 | } 21 | } else { 22 | return res.forbidden('Wrong authorization format.'); 23 | } 24 | } else if (req.param('token')) { 25 | token = req.param('token'); 26 | // We delete the token from param to not mess with blueprints 27 | delete req.query.token; 28 | } else { 29 | return res.forbidden('No authorization header found.'); 30 | } 31 | 32 | AuthToken.verifyToken(token, function(err, decodedToken) { 33 | if (err) return res.forbidden('Invalid Token.'); 34 | req.token = decodedToken.sub; 35 | next(); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /api/policies/noCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets no-cache header in response. 3 | */ 4 | module.exports = function (req, res, next) { 5 | sails.log.info("Applying disable cache policy"); 6 | res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); 7 | res.header('Expires', '-1'); 8 | res.header('Pragma', 'no-cache'); 9 | next(); 10 | }; 11 | -------------------------------------------------------------------------------- /api/responses/badRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 400 (Bad Request) Handler 3 | * 4 | * Usage: 5 | * return res.badRequest(); 6 | * return res.badRequest(data); 7 | * return res.badRequest(data, 'some/specific/badRequest/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.badRequest( 12 | * 'Please choose a valid `password` (6-12 characters)', 13 | * 'trial/signup' 14 | * ); 15 | * ``` 16 | */ 17 | 18 | module.exports = function badRequest(data, options) { 19 | 20 | // Get access to `req`, `res`, & `sails` 21 | var req = this.req; 22 | var res = this.res; 23 | var sails = req._sails; 24 | 25 | // Set status code 26 | res.status(400); 27 | 28 | // Log error to console 29 | if (data !== undefined) { 30 | sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); 31 | } 32 | else sails.log.verbose('Sending 400 ("Bad Request") response'); 33 | 34 | // Only include errors in response if application environment 35 | // is not set to 'production'. In production, we shouldn't 36 | // send back any identifying information about errors. 37 | if (sails.config.environment === 'production') { 38 | data = undefined; 39 | } 40 | 41 | // If the user-agent wants JSON, always respond with JSON 42 | if (req.wantsJSON) { 43 | return res.json(data); 44 | } 45 | 46 | // If second argument is a string, we take that to mean it refers to a view. 47 | // If it was omitted, use an empty object (`{}`) 48 | options = (typeof options === 'string') ? { view: options } : options || {}; 49 | 50 | // If a view was provided in options, serve it. 51 | // Otherwise try to guess an appropriate view, or if that doesn't 52 | // work, just send JSON. 53 | if (options.view) { 54 | return res.view(options.view, { data: data }); 55 | } 56 | 57 | // If no second argument provided, try to serve the implied view, 58 | // but fall back to sending JSON(P) if no view can be inferred. 59 | else return res.guessView({ data: data }, function couldNotGuessView () { 60 | return res.json(data); 61 | }); 62 | 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /api/responses/forbidden.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 403 (Forbidden) Handler 3 | * 4 | * Usage: 5 | * return res.forbidden(); 6 | * return res.forbidden(err); 7 | * return res.forbidden(err, 'some/specific/forbidden/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.forbidden('Access denied.'); 12 | * ``` 13 | */ 14 | 15 | module.exports = function forbidden (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(403); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); 28 | } 29 | else sails.log.verbose('Sending 403 ("Forbidden") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production') { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | if (req.wantsJSON) { 40 | return res.json(data); 41 | } 42 | 43 | // If second argument is a string, we take that to mean it refers to a view. 44 | // If it was omitted, use an empty object (`{}`) 45 | options = (typeof options === 'string') ? { view: options } : options || {}; 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: data }); 52 | } 53 | 54 | // If no second argument provided, try to serve the default view, 55 | // but fall back to sending JSON(P) if any errors occur. 56 | else return res.view('403', { data: data }, function (err, html) { 57 | 58 | // If a view error occured, fall back to JSON(P). 59 | if (err) { 60 | // 61 | // Additionally: 62 | // • If the view was missing, ignore the error but provide a verbose log. 63 | if (err.code === 'E_VIEW_FAILED') { 64 | sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); 65 | } 66 | // Otherwise, if this was a more serious error, log to the console with the details. 67 | else { 68 | sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 69 | } 70 | return res.json(data); 71 | } 72 | 73 | return res.send(html); 74 | }); 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /api/responses/notFound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 404 (Not Found) Handler 3 | * 4 | * Usage: 5 | * return res.notFound(); 6 | * return res.notFound(err); 7 | * return res.notFound(err, 'some/specific/notfound/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.notFound(); 12 | * ``` 13 | * 14 | * NOTE: 15 | * If a request doesn't match any explicit routes (i.e. `config/routes.js`) 16 | * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` 17 | * automatically. 18 | */ 19 | 20 | module.exports = function notFound (data, options) { 21 | 22 | // Get access to `req`, `res`, & `sails` 23 | var req = this.req; 24 | var res = this.res; 25 | var sails = req._sails; 26 | 27 | // Set status code 28 | res.status(404); 29 | 30 | // Log error to console 31 | if (data !== undefined) { 32 | sails.log.verbose('Sending 404 ("Not Found") response: \n',data); 33 | } 34 | else sails.log.verbose('Sending 404 ("Not Found") response'); 35 | 36 | // Only include errors in response if application environment 37 | // is not set to 'production'. In production, we shouldn't 38 | // send back any identifying information about errors. 39 | if (sails.config.environment === 'production') { 40 | data = undefined; 41 | } 42 | 43 | // If the user-agent wants JSON, always respond with JSON 44 | if (req.wantsJSON) { 45 | return res.json(data); 46 | } 47 | 48 | // If second argument is a string, we take that to mean it refers to a view. 49 | // If it was omitted, use an empty object (`{}`) 50 | options = (typeof options === 'string') ? { view: options } : options || {}; 51 | 52 | // If a view was provided in options, serve it. 53 | // Otherwise try to guess an appropriate view, or if that doesn't 54 | // work, just send JSON. 55 | if (options.view) { 56 | return res.view(options.view, { data: data }); 57 | } 58 | 59 | // If no second argument provided, try to serve the default view, 60 | // but fall back to sending JSON(P) if any errors occur. 61 | else return res.view('homepage', {}, function (err, html) { 62 | 63 | // If a view error occured, fall back to JSON(P). 64 | if (err) { 65 | // 66 | // Additionally: 67 | // • If the view was missing, ignore the error but provide a verbose log. 68 | if (err.code === 'E_VIEW_FAILED') { 69 | sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); 70 | } 71 | // Otherwise, if this was a more serious error, log to the console with the details. 72 | else { 73 | sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 74 | } 75 | return res.json(data); 76 | } 77 | 78 | return res.send(html); 79 | }); 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /api/responses/ok.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 200 (OK) Response 3 | * 4 | * Usage: 5 | * return res.ok(); 6 | * return res.ok(data); 7 | * return res.ok(data, 'auth/login'); 8 | * 9 | * @param {Object} data 10 | * @param {String|Object} options 11 | * - pass string to render specified view 12 | */ 13 | 14 | module.exports = function sendOK (data, options) { 15 | 16 | // Get access to `req`, `res`, & `sails` 17 | var req = this.req; 18 | var res = this.res; 19 | var sails = req._sails; 20 | 21 | sails.log.silly('res.ok() :: Sending 200 ("OK") response'); 22 | 23 | // Set status code 24 | res.status(200); 25 | 26 | // If appropriate, serve data as JSON(P) 27 | if (req.wantsJSON) { 28 | return res.json(data); 29 | } 30 | 31 | // If second argument is a string, we take that to mean it refers to a view. 32 | // If it was omitted, use an empty object (`{}`) 33 | options = (typeof options === 'string') ? { view: options } : options || {}; 34 | 35 | // If a view was provided in options, serve it. 36 | // Otherwise try to guess an appropriate view, or if that doesn't 37 | // work, just send JSON. 38 | if (options.view) { 39 | return res.view(options.view, { data: data }); 40 | } 41 | 42 | // If no second argument provided, try to serve the implied view, 43 | // but fall back to sending JSON(P) if no view can be inferred. 44 | else return res.guessView({ data: data }, function couldNotGuessView () { 45 | return res.json(data); 46 | }); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /api/responses/serverError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 500 (Server Error) Response 3 | * 4 | * Usage: 5 | * return res.serverError(); 6 | * return res.serverError(err); 7 | * return res.serverError(err, 'some/specific/error/view'); 8 | * 9 | * NOTE: 10 | * If something throws in a policy or controller, or an internal 11 | * error is encountered, Sails will call `res.serverError()` 12 | * automatically. 13 | */ 14 | 15 | module.exports = function serverError(data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(500); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.error('Sending 500 ("Server Error") response: \n', data); 28 | } else { 29 | sails.log.error('Sending empty 500 ("Server Error") response'); 30 | } 31 | 32 | // Only include errors in response if application environment 33 | // is not set to 'production'. In production, we shouldn't 34 | // send back any identifying information about errors. 35 | if (sails.config.environment === 'production') { 36 | data = undefined; 37 | } 38 | 39 | // If the user-agent wants JSON, always respond with JSON 40 | if (req.wantsJSON) { 41 | return res.json(data); 42 | } 43 | 44 | // If second argument is a string, we take that to mean it refers to a view. 45 | // If it was omitted, use an empty object (`{}`) 46 | options = (typeof options === 'string') ? { 47 | view: options 48 | } : options || {}; 49 | 50 | // If a view was provided in options, serve it. 51 | // Otherwise try to guess an appropriate view, or if that doesn't 52 | // work, just send JSON. 53 | if (options.view) { 54 | return res.view(options.view, { 55 | data: data 56 | }); 57 | } 58 | 59 | // If no second argument provided, try to serve the default view, 60 | // but fall back to sending JSON(P) if any errors occur. 61 | else return res.view('500', { 62 | data: data 63 | }, function(err, html) { 64 | 65 | // If a view error occured, fall back to JSON(P). 66 | if (err) { 67 | // 68 | // Additionally: 69 | // • If the view was missing, ignore the error but provide a verbose log. 70 | if (err.code === 'E_VIEW_FAILED') { 71 | sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ', err); 72 | } 73 | // Otherwise, if this was a more serious error, log to the console with the details. 74 | else { 75 | sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 76 | } 77 | return res.json(data); 78 | } 79 | 80 | return res.send(html); 81 | }); 82 | 83 | }; 84 | -------------------------------------------------------------------------------- /api/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/api/services/.gitkeep -------------------------------------------------------------------------------- /api/services/AssetService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File Service 3 | * 4 | * Handles uploads & downloads of versions 5 | */ 6 | 7 | var mime = require('mime'); 8 | 9 | var fsx = require('fs-extra'); 10 | var crypto = require('crypto'); 11 | var Promise = require('bluebird'); 12 | 13 | var SkipperDisk = require('skipper-disk'); 14 | 15 | var AssetService = {}; 16 | 17 | AssetService.serveFile = function (req, res, asset) { 18 | // Stream the file to the user 19 | fsx.createReadStream(asset.fd) 20 | .on('error', function (err) { 21 | res.serverError('An error occurred while accessing asset.', err); 22 | sails.log.error('Unable to access asset:', asset.fd); 23 | }) 24 | .on('open', function () { 25 | // Send file properties in header 26 | res.setHeader( 27 | 'Content-Disposition', 'attachment; filename*=UTF-8\'\'' + encodeURIComponent(asset.name) 28 | ); 29 | res.setHeader('Content-Length', asset.size); 30 | res.setHeader('Content-Type', mime.getType(asset.fd)); 31 | }) 32 | .on('end', async function complete() { 33 | // After we have sent the file, log analytics, failures experienced at 34 | // this point should only be handled internally (do not use the res 35 | // object). 36 | // 37 | // Atomically increment the download count for analytics purposes 38 | // 39 | // Warning: not all adapters support queries (such as sails-disk). 40 | var datastore = Asset.getDatastore(); 41 | if (_.isFunction(datastore.sendNativeQuery) && datastore.config.adapter !== 'sails-disk') { 42 | try { 43 | await datastore.sendNativeQuery( 44 | 'UPDATE asset SET download_count = download_count + 1 WHERE id = $1;', [asset.id]) 45 | .intercept(function (err) { 46 | }); 47 | 48 | // Early exit if the query was successful. 49 | return; 50 | } catch (err) { 51 | sails.log.error( 52 | 'An error occurred while logging asset download', err 53 | ); 54 | } 55 | } 56 | 57 | // Attempt to update the download count through the fallback mechanism. 58 | // Note that this may be lossy since it is not atomic. 59 | asset.download_count++; 60 | 61 | Asset.update({ 62 | id: asset.id 63 | }, asset) 64 | .exec(function (err) { 65 | if (err) { 66 | sails.log.error( 67 | 'An error occurred while logging asset download', err 68 | ); 69 | } 70 | }); 71 | }) 72 | // Pipe to user 73 | .pipe(res); 74 | }; 75 | 76 | /** 77 | * Asyncronously generates a SHA1 hash from a file 78 | * Identical to: 79 | * https://github.com/electron-userland/electron-builder/blob/552f1a4ed6f4bb83c3c548ed962c21142f07a9b4/packages/electron-updater/src/DownloadedUpdateHelper.ts#L161 80 | * @param {String} fd File descriptor of file to hash 81 | * @return {String} Promise which is resolved with the hash once complete 82 | */ 83 | AssetService.getHash = function (fd, type = "sha1", encoding = "hex") { 84 | return new Promise(function (resolve, reject) { 85 | var hash = crypto.createHash(type); 86 | hash.setEncoding(encoding); 87 | 88 | fsx 89 | .createReadStream(fd) 90 | .on("error", function (err) { 91 | reject(err); 92 | }) 93 | .on("end", function () { 94 | hash.end(); 95 | resolve(hash.read()); 96 | }) 97 | // Pipe to hash generator 98 | .pipe(hash, { end: false }); 99 | }); 100 | }; 101 | 102 | 103 | /** 104 | * Deletes an asset from the database. 105 | * Warning: this will NOT remove fd from the file system. 106 | * @param {Record} asset The asset's record object from sails 107 | * @param {Object} req Optional: The request object 108 | * @returns {Promise} Resolved once the asset is destroyed 109 | */ 110 | AssetService.destroy = function (asset, req) { 111 | if (!asset) { 112 | throw new Error('You must pass an asset'); 113 | } 114 | 115 | return Asset.destroy(asset.id) 116 | .then(function destroyedRecord() { 117 | if (sails.hooks.pubsub) { 118 | Asset.publish( 119 | [asset.id], 120 | { 121 | verb: 'destroyed', 122 | previous: asset 123 | }, 124 | !req._sails.config.blueprints.mirror && req 125 | ); 126 | 127 | if (req && req.isSocket) { 128 | Asset.unsubscribe(req, record); 129 | Asset.retire(record); 130 | } 131 | } 132 | }); 133 | }; 134 | 135 | /** 136 | * Deletes an asset's file from the filesystem. 137 | * Warning: this will NOT remove the reference to the fd in the database. 138 | * @param {Object} asset The asset object who's file we would like deleted 139 | * @returns {Promise} Resolved once the file is deleted 140 | */ 141 | AssetService.deleteFile = function (asset) { 142 | if (!asset) { 143 | throw new Error('You must pass an asset'); 144 | } 145 | if (!asset.fd) { 146 | throw new Error('The provided asset does not have a file descriptor'); 147 | } 148 | 149 | var fileAdapter = SkipperDisk(); 150 | var fileAdapterRmAsync = Promise.promisify(fileAdapter.rm); 151 | 152 | return fileAdapterRmAsync(asset.fd); 153 | }; 154 | 155 | module.exports = AssetService; 156 | -------------------------------------------------------------------------------- /api/services/AuthToken.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken'); 2 | 3 | module.exports.issueToken = function(payload, options) { 4 | var token = jwt.sign(payload, process.env.TOKEN_SECRET || sails.config.jwt.token_secret, options); 5 | return token; 6 | }; 7 | 8 | module.exports.verifyToken = function(token, callback) { 9 | return jwt.verify(token, process.env.TOKEN_SECRET || sails.config.jwt.token_secret, {}, callback); 10 | }; 11 | -------------------------------------------------------------------------------- /api/services/ChannelService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Platform Service 3 | */ 4 | 5 | var _ = require('lodash'); 6 | 7 | var ChannelService = { 8 | availableChannels: sails.config.channels 9 | }; 10 | 11 | /** 12 | * Retrieves all available channels given the most restrictive one 13 | * @param {String} channel Channel name 14 | * @return {Array} Applicable channel names ordered by desc. stability 15 | */ 16 | ChannelService.getApplicableChannels = function(channel) { 17 | var channelIndex = ChannelService.availableChannels.indexOf(channel); 18 | 19 | if (channelIndex === -1) { 20 | return ChannelService.availableChannels[ChannelService.availableChannels.length - 1]; 21 | } 22 | 23 | return ChannelService.availableChannels.slice( 24 | 0, 25 | channelIndex + 1 26 | ); 27 | }; 28 | 29 | module.exports = ChannelService; 30 | -------------------------------------------------------------------------------- /api/services/FlavorService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flavor Service 3 | */ 4 | 5 | const FlavorService = {}; 6 | 7 | /** 8 | * Deletes a flavor from the database. 9 | * @param {Object} flavor The flavors record object from sails 10 | * @param {Object} req Optional: The request object 11 | * @returns {Promise} Resolved once the flavor is destroyed 12 | */ 13 | FlavorService.destroy = (flavor, req) => { 14 | if (!flavor) { 15 | throw new Error('You must pass a flavor'); 16 | } 17 | 18 | return Flavor 19 | .destroy(flavor.name) 20 | .then(() => { 21 | if (sails.hooks.pubsub) { 22 | Flavor.publish( 23 | [flavor.name], { 24 | verb: 'destroyed', 25 | previous: flavor 26 | }, !req._sails.config.blueprints.mirror && req 27 | ); 28 | 29 | if (req && req.isSocket) { 30 | Flavor.unsubscribe(req, flavor); 31 | Flavor.retire(flavor); 32 | } 33 | } 34 | }); 35 | }; 36 | 37 | module.exports = FlavorService; 38 | -------------------------------------------------------------------------------- /api/services/PlatformService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Platform Service 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var useragent = require('express-useragent'); 7 | 8 | var PlatformService = { 9 | LINUX: 'linux', 10 | LINUX_32: 'linux_32', 11 | LINUX_64: 'linux_64', 12 | OSX: 'osx', 13 | OSX_32: 'osx_32', 14 | OSX_64: 'osx_64', 15 | OSX_ARM64: 'osx_arm64', 16 | WINDOWS: 'windows', 17 | WINDOWS_32: 'windows_32', 18 | WINDOWS_64: 'windows_64', 19 | }; 20 | 21 | /** 22 | * Reduce a platfrom ID to its type 23 | * (ex. windows_64 to windows) 24 | * @param {String} platform Platform ID 25 | * @return {String} Platform type name 26 | */ 27 | PlatformService.toType = function(platform) { 28 | return _.head(platform.split('_')); 29 | }; 30 | 31 | /** 32 | * Detect the user's platform from a request object 33 | * @param {Object} req An express request object 34 | * @return {String} String representation of the detected platform 35 | */ 36 | PlatformService.detectFromRequest = function(req) { 37 | var source = req.headers['user-agent']; 38 | var ua = useragent.parse(source); 39 | 40 | if (ua.isWindows) return [this.WINDOWS_32, this.WINDOWS_64]; 41 | if (ua.isMac) return [this.OSX_64, this.OSX_ARM64]; // this.OSX_ARM64 until a bug with arm64 useragent is fixed 42 | if (ua.isLinux64) return [this.LINUX_64, this.LINUX_32]; 43 | if (ua.isLinux) return [this.LINUX_32]; 44 | }; 45 | 46 | /** 47 | * Detect and normalize the platformID from platform name input string. 48 | * Used to handle unnormalized inputs from user agents. 49 | * @param {Object} platformName An Asset object 50 | * @param {Boolean} strictMatch Whether to only match to the current arch 51 | * If false, 32 bit will be added for 64 bit 52 | * @return {String} Full platform ID 53 | */ 54 | PlatformService.detect = function(platformName, strictMatch) { 55 | var name = platformName.toLowerCase(); 56 | var prefix = ''; 57 | var suffixes = []; 58 | 59 | // Detect prefix: osx, widnows or linux 60 | if (_.includes(name, 'win')) { 61 | prefix = this.WINDOWS; 62 | } 63 | 64 | if ( 65 | _.includes(name, 'linux') || 66 | _.includes(name, 'ubuntu') 67 | ) { 68 | prefix = this.LINUX; 69 | } 70 | 71 | if ( 72 | _.includes(name, 'mac') || 73 | _.includes(name, 'osx') || 74 | name.indexOf('darwin') >= 0 75 | ) { 76 | prefix = this.OSX; 77 | } 78 | 79 | // Detect architecture 80 | if ( 81 | prefix === this.OSX || 82 | // _.includes(name, 'x64') || 83 | // _.includes(name, 'amd64') || 84 | _.includes(name, '64') 85 | ) { 86 | if (_.includes(name, 'arm64')) { 87 | suffixes.unshift('arm64'); 88 | } else { 89 | suffixes.push('64'); 90 | } 91 | 92 | if (!strictMatch && prefix !== this.OSX) { 93 | suffixes.unshift('32'); 94 | } 95 | } else { 96 | suffixes.unshift('32'); 97 | } 98 | 99 | var result = []; 100 | _.forEach(suffixes, function(suffix) { 101 | result.push(prefix + '_' + suffix); 102 | }); 103 | 104 | return result; 105 | }; 106 | 107 | PlatformService.sanitize = function(platforms) { 108 | var self = this; 109 | return _.map(platforms, function(platform) { 110 | switch (platform) { 111 | case self.OSX: 112 | case self.OSX_32: 113 | platform = self.OSX_64; 114 | break; 115 | default: 116 | } 117 | 118 | return platform; 119 | }); 120 | } 121 | 122 | module.exports = PlatformService; 123 | -------------------------------------------------------------------------------- /api/services/UtilityService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple service for making reusable helper functions globaly available yet 3 | * namespaced 4 | */ 5 | 6 | var _ = require('lodash'); 7 | var semver = require('semver'); 8 | 9 | var UtilityService = {}; 10 | 11 | UtilityService.getTruthyObject = function(object) { 12 | return _.pickBy(object, _.identity); 13 | }; 14 | 15 | /** 16 | * Compare version objects using semantic versioning. 17 | * Pass to Array.sort for a descending array 18 | * @param {Object} v1 Version object one 19 | * @param {Object} v2 Version object two 20 | * @return {-1|0|1} Whether one is is less than or greater 21 | */ 22 | UtilityService.compareVersion = function(v1, v2) { 23 | return -semver.compare(v1.name, v2.name); 24 | }; 25 | 26 | module.exports = UtilityService; 27 | -------------------------------------------------------------------------------- /api/services/VersionService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Version Service 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var semver = require('semver'); 7 | 8 | var VersionService = {}; 9 | 10 | // Normalize version name 11 | VersionService.normalizeName = function(name) { 12 | if (name[0] == 'v') name = name.slice(1); 13 | return name; 14 | }; 15 | 16 | // Compare two versions 17 | VersionService.compare = function(v1, v2) { 18 | if (semver.gt(v1.tag, v2.tag)) { 19 | return -1; 20 | } 21 | if (semver.lt(v1.tag, v2.tag)) { 22 | return 1; 23 | } 24 | return 0; 25 | }; 26 | 27 | /** 28 | * Deletes a version from the database. 29 | * @param {Object} version The versions record object from sails 30 | * @param {Object} req Optional: The request object 31 | * @returns {Promise} Resolved once the version is destroyed 32 | */ 33 | VersionService.destroy = (version, req) => { 34 | if (!version) { 35 | throw new Error('You must pass a version'); 36 | } 37 | 38 | return Version 39 | .destroy(version.id) 40 | .then(() => { 41 | if (sails.hooks.pubsub) { 42 | Version.publish( 43 | [version.id], { 44 | verb: 'destroyed', 45 | previous: version 46 | }, !req._sails.config.blueprints.mirror && req 47 | ); 48 | 49 | if (req && req.isSocket) { 50 | Version.unsubscribe(req, version); 51 | Version.retire(version); 52 | } 53 | } 54 | }); 55 | }; 56 | 57 | module.exports = VersionService; 58 | -------------------------------------------------------------------------------- /api/services/WindowsReleaseService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Windows Release Service 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var semver = require('semver'); 7 | var stripBom = require('strip-bom'); 8 | 9 | var ChannelService = require('./ChannelService'); 10 | 11 | var WindowsReleaseService = {}; 12 | 13 | // Ordered list of supported pre-release channels 14 | var PRERELEASE_CHANNEL_MAGINITUDE = 1000; 15 | var PRERELEASE_CHANNELS = _(ChannelService.availableChannels) 16 | .without('stable') 17 | .reverse() 18 | .value(); 19 | 20 | // RELEASES parsing 21 | var releaseRegex = /^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$/; 22 | 23 | /** 24 | * Hash a prerelease 25 | * @param {String} s [description] 26 | * @return {String} [description] 27 | */ 28 | function hashPrerelease(s) { 29 | if (_.isString(s[0])) { 30 | return (_.indexOf(PRERELEASE_CHANNELS, s[0]) + 1) * PRERELEASE_CHANNEL_MAGINITUDE + (s[1] || 0); 31 | } else { 32 | return s[0]; 33 | } 34 | } 35 | 36 | // Map a semver version to a windows version 37 | WindowsReleaseService.normVersion = function(tag) { 38 | var parts = new semver.SemVer(tag); 39 | var prerelease = ''; 40 | 41 | if (parts.prerelease && parts.prerelease.length > 0) { 42 | prerelease = hashPrerelease(parts.prerelease); 43 | } 44 | 45 | return [ 46 | parts.major, 47 | parts.minor, 48 | parts.patch 49 | ].join('.') + (prerelease ? '.' + prerelease : ''); 50 | }; 51 | 52 | // Map a windows version to a semver 53 | WindowsReleaseService.toSemver = function(tag) { 54 | var parts = tag.split('.'); 55 | var version = parts.slice(0, 3).join('.'); 56 | var prerelease = Number(parts[3]); 57 | 58 | // semver == windows version 59 | if (!prerelease) return version; 60 | 61 | var channelId = Math.floor(prerelease / CHANNEL_MAGINITUDE); 62 | var channel = CHANNELS[channelId - 1]; 63 | var count = prerelease - (channelId * CHANNEL_MAGINITUDE); 64 | 65 | return version + '-' + channel + '.' + count; 66 | }; 67 | 68 | // Parse RELEASES file 69 | // https://github.com/Squirrel/Squirrel.Windows/blob/0d1250aa6f0c25fe22e92add78af327d1277d97d/src/Squirrel/ReleaseExtensions.cs#L19 70 | WindowsReleaseService.parse = function(content) { 71 | return _.chain(stripBom(content)) 72 | .replace('\r\n', '\n') 73 | .split('\n') 74 | .map(function(line) { 75 | var parts = releaseRegex.exec(line); 76 | if (!parts) return null; 77 | 78 | var filename = parts[2]; 79 | var isDelta = filename.indexOf('-full.nupkg') == -1; 80 | 81 | var filenameParts = filename 82 | .replace('.nupkg', '') 83 | .replace('-delta', '') 84 | .replace('-full', '') 85 | .split(/\.|-/) 86 | .reverse(); 87 | 88 | var version = _.chain(filenameParts) 89 | .filter(function(x) { 90 | return /^\d+$/.exec(x); 91 | }) 92 | .reverse() 93 | .value() 94 | .join('.'); 95 | 96 | return { 97 | sha: parts[1], 98 | filename: filename, 99 | size: Number(parts[3]), 100 | isDelta: isDelta, 101 | version: version, 102 | semver: this.toSemver(version) 103 | }; 104 | }) 105 | .compact() 106 | .value(); 107 | }; 108 | 109 | // Generate a RELEASES file 110 | WindowsReleaseService.generate = function(assets) { 111 | return _.map(assets, function(asset) { 112 | return [ 113 | asset.hash, 114 | asset.name.replace('-ia32', ''), 115 | asset.size 116 | ].join(' '); 117 | }) 118 | .join('\n'); 119 | }; 120 | 121 | module.exports = WindowsReleaseService; 122 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Use `app.js` to run your app without `sails lift`. 5 | * To start the server, run: `node app.js`. 6 | * 7 | * This is handy in situations where the sails CLI is not relevant or useful. 8 | * 9 | * For example: 10 | * => `node app.js` 11 | * => `forever start app.js` 12 | * => `node debug app.js` 13 | * => `modulus deploy` 14 | * => `heroku scale` 15 | * 16 | * 17 | * The same command-line arguments are supported, e.g.: 18 | * `node app.js --silent --port=80 --prod` 19 | */ 20 | 21 | // Ensure we're in the project directory, so relative paths work as expected 22 | // no matter where we actually lift from. 23 | process.chdir(__dirname); 24 | 25 | // Ensure a "sails" can be located: 26 | (function() { 27 | var sails; 28 | try { 29 | sails = require('sails'); 30 | } catch (e) { 31 | console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); 32 | console.error('To do that, run `npm install sails`'); 33 | console.error(''); 34 | console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); 35 | console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); 36 | console.error('but if it doesn\'t, the app will run with the global sails instead!'); 37 | return; 38 | } 39 | 40 | // Try to get `rc` dependency 41 | var rc; 42 | try { 43 | rc = require('rc'); 44 | } catch (e0) { 45 | try { 46 | rc = require('sails/node_modules/rc'); 47 | } catch (e1) { 48 | console.error('Could not find dependency: `rc`.'); 49 | console.error('Your `.sailsrc` file(s) will be ignored.'); 50 | console.error('To resolve this, run:'); 51 | console.error('npm install rc --save'); 52 | rc = function () { return {}; }; 53 | } 54 | } 55 | 56 | 57 | // Start server 58 | sails.lift(rc('sails')); 59 | })(); 60 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/assets/favicon.ico -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/assets/images/.gitkeep -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/images/menu-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /assets/js/admin/add-flavor-modal/add-flavor-modal-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.add-flavor-modal', []) 2 | .controller('AddFlavorModalController', ['$scope', 'DataService', '$uibModalInstance', 3 | ($scope, DataService, $uibModalInstance) => { 4 | $scope.flavor = { 5 | name: '' 6 | }; 7 | 8 | $scope.addFlavor = () => { 9 | DataService.createFlavor($scope.flavor) 10 | .then( 11 | $uibModalInstance.close, 12 | () => {} 13 | ); 14 | }; 15 | 16 | $scope.closeModal = $uibModalInstance.dismiss; 17 | } 18 | ]); 19 | -------------------------------------------------------------------------------- /assets/js/admin/add-flavor-modal/add-flavor-modal.pug: -------------------------------------------------------------------------------- 1 | .modal-header 2 | h3.modal-title Add a New Flavor 3 | .modal-body 4 | form.form-horizontal(novalidate='') 5 | fieldset 6 | // Flavor Name 7 | .form-group 8 | label.col-md-4.control-label(for='name') Flavor Name 9 | .col-md-7 10 | input.form-control.input-md( 11 | ng-model='flavor.name', 12 | minlength='1', 13 | required 14 | ) 15 | .modal-footer 16 | div 17 | button.btn.btn-warning(type='button', ng-click='addFlavor()') Add Flavor 18 | button.btn.btn-danger(type='button', ng-click='closeModal()') Cancel 19 | -------------------------------------------------------------------------------- /assets/js/admin/add-version-asset-modal/add-version-asset-modal-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.add-version-asset-modal', []) 2 | .controller('AddVersionAssetModalController', ['$scope', 'DataService', '$uibModalInstance', 'version', 3 | ($scope, DataService, $uibModalInstance, version) => { 4 | $scope.DataService = DataService; 5 | 6 | $scope.versionName = version.name; 7 | $scope.versionIsAvailable = DataService.checkAvailability(version); 8 | 9 | $scope.asset = { 10 | name: '', 11 | platform: '', 12 | file: null 13 | }; 14 | 15 | $scope.addAsset = function() { 16 | if (!$scope.asset.name) { 17 | delete $scope.asset.name; 18 | } 19 | 20 | DataService.createAsset($scope.asset, version.id) 21 | .then(function success(response) { 22 | $uibModalInstance.close(); 23 | }, function error(response) { 24 | $uibModalInstance.dismiss(); 25 | }); 26 | }; 27 | 28 | $scope.updateAssetName = () => { 29 | $scope.namePlaceholder = $scope.asset.file && $scope.asset.file.name; 30 | }; 31 | 32 | $scope.closeModal = function() { 33 | $uibModalInstance.dismiss(); 34 | }; 35 | } 36 | ]); 37 | -------------------------------------------------------------------------------- /assets/js/admin/add-version-asset-modal/add-version-asset-modal.pug: -------------------------------------------------------------------------------- 1 | form(name='assetForm', role='form') 2 | .modal-header 3 | h3.modal-title Add an asset to {{ versionName }} 4 | .modal-body 5 | .form-horizontal 6 | // Asset File 7 | .form-group 8 | label.col-md-4.control-label(for='name') Asset Name 9 | .col-md-7 10 | input.form-control.input-md( 11 | ng-model='asset.name', 12 | name='name', 13 | placeholder='{{ namePlaceholder || "Auto-populated with filename" }}' 14 | ) 15 | // Select platform 16 | .form-group 17 | label.col-md-4.control-label(for='platform') Platform 18 | .col-md-7 19 | select.form-control( 20 | ng-model='asset.platform', 21 | name='platform', 22 | ng-options='key as value for (key, value) in DataService.availablePlatforms', 23 | required 24 | ) 25 | // Select File 26 | .form-group 27 | label.col-md-4.control-label(for='file') File 28 | .col-md-7 29 | input.form-control( 30 | type='file', 31 | ngf-select='', 32 | ngf-change='updateAssetName()', 33 | ng-model='asset.file', 34 | name='file', 35 | ngf-accept="'.dmg,.zip,.deb,.exe,.nupkg,.blockmap'", 36 | ngf-max-size='500MB', 37 | required='', 38 | ) 39 | .modal-footer 40 | .modal-footer-note.col-md-7 41 | span.text-gray(ng-show='!asset.file.progress && versionIsAvailable') 42 | | This file will be live once uploaded. 43 | uib-progressbar.progress-striped.active( 44 | type="success" 45 | value='asset.file.progress', 46 | ng-show='asset.file.progress >= 0' 47 | ) 48 | strong {{ asset.file.progress }}% 49 | div 50 | button.btn.btn-warning(type='button', ng-disabled="!assetForm.$valid" ng-click='addAsset()') Upload 51 | button.btn.btn-danger(type='button', ng-click='closeModal()') Cancel 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /assets/js/admin/add-version-modal/add-version-modal-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.add-version-modal', []) 2 | .controller('AddVersionModalController', ['$scope', '$http', 'DataService', 'Notification', '$uibModalInstance', 'PubSub', 'moment', 3 | ($scope, $http, DataService, Notification, $uibModalInstance, PubSub, moment) => { 4 | $scope.availableChannels = DataService.availableChannels; 5 | $scope.currentDateTime = moment().startOf('second').toDate(); 6 | $scope.availableFlavors = DataService.availableFlavors; 7 | 8 | $scope.version = { 9 | id: '', 10 | name: '', 11 | notes: '', 12 | channel: { 13 | name: DataService.availableChannels[0] 14 | }, 15 | availability: $scope.currentDateTime, 16 | flavor: { 17 | name: 'default' 18 | } 19 | }; 20 | 21 | $scope.addVersion = function() { 22 | DataService.createVersion($scope.version) 23 | .then(function success(response) { 24 | $uibModalInstance.close(); 25 | }, function error(response) {}); 26 | }; 27 | 28 | $scope.closeModal = function() { 29 | $uibModalInstance.dismiss(); 30 | }; 31 | 32 | // Watch for changes to data content and update local data accordingly. 33 | var uid1 = PubSub.subscribe('data-change', function() { 34 | $scope.availableChannels = DataService.availableChannels; 35 | $scope.availableFlavors = DataService.availableFlavors; 36 | 37 | $scope.version = { 38 | id: '', 39 | name: '', 40 | notes: '', 41 | channel: { 42 | name: DataService.availableChannels[0] 43 | }, 44 | availability: $scope.currentDateTime, 45 | flavor: { 46 | name: 'default' 47 | } 48 | }; 49 | }); 50 | 51 | $scope.$on('$destroy', function() { 52 | PubSub.unsubscribe(uid1); 53 | }); 54 | } 55 | ]); 56 | -------------------------------------------------------------------------------- /assets/js/admin/add-version-modal/add-version-modal.pug: -------------------------------------------------------------------------------- 1 | .modal-header 2 | h3.modal-title Add a New Version 3 | .modal-body 4 | form.form-horizontal(novalidate='') 5 | fieldset 6 | // Version Name 7 | .form-group 8 | label.col-md-4.control-label(for='name') Version Name 9 | .col-md-7 10 | input.form-control.input-md( 11 | ng-model='version.name', 12 | minlength='1', 13 | required 14 | ) 15 | span.help-block This must match the 16 | a(href='http://semver.org/') semver format 17 | | . 18 | em ex. 1.0.1 19 | span.help-block( 20 | ng-hide="version.channel.name === 'stable'" 21 | ) You are using a 22 | span.text-warning pre-release channel 23 | | , it 24 | strong.text-success must 25 | | follow the version triple, preceded by a dash. It may additionally be followed by a 26 | span.text-info whole number. 27 | br 28 | em ex. 1.0.1 29 | span.text-warning -{{version.channel.name}}. 30 | span.text-info 1 31 | // Select Flavor 32 | .form-group 33 | label.col-md-4.control-label(for='flavor') Flavor 34 | .col-md-7 35 | select.form-control( 36 | ng-model='version.flavor.name', 37 | name='flavor', 38 | ng-options='flavor for flavor in availableFlavors', 39 | required 40 | ) 41 | // Select Channel 42 | .form-group 43 | label.col-md-4.control-label(for='channel') Channel 44 | .col-md-7 45 | select.form-control( 46 | ng-model='version.channel.name', 47 | name='channel', 48 | ng-options='o as o for o in availableChannels', 49 | required 50 | ) 51 | hr 52 | span.help-block You will not be able to change the above later. 53 | // Availability Date 54 | .form-group 55 | label.col-md-4.control-label(for='availability') Availability Date 56 | .col-md-7 57 | input.form-control.input-md( 58 | ng-model='version.availability', 59 | name='availability', 60 | type='datetime-local', 61 | min='{{ currentDateTime }}', 62 | step='1', 63 | required 64 | ) 65 | // Change Notes 66 | .form-group 67 | label.col-md-4.control-label(for='notes') Change Notes 68 | .col-md-7 69 | textarea.form-control(ng-model='version.notes', name='notes') 70 | .modal-footer 71 | div 72 | button.btn.btn-warning(type='button', ng-click='addVersion()') Add Version 73 | button.btn.btn-danger(type='button', ng-click='closeModal()') Cancel 74 | -------------------------------------------------------------------------------- /assets/js/admin/admin.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin', [ 2 | 'app.admin.version-table', 3 | 'app.admin.add-flavor-modal', 4 | 'app.admin.add-version-modal', 5 | 'app.admin.add-version-asset-modal', 6 | 'app.admin.edit-version-modal', 7 | 'app.admin.edit-version-asset-modal' 8 | ]); 9 | -------------------------------------------------------------------------------- /assets/js/admin/edit-version-asset-modal/edit-version-asset-modal-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.edit-version-asset-modal', []) 2 | .controller('EditVersionAssetModalController', ['$scope', 'DataService', 'Notification', '$uibModalInstance', 'asset', 3 | function($scope, DataService, Notification, $uibModalInstance, asset) { 4 | $scope.DataService = DataService; 5 | 6 | $scope.originalName = asset.name; 7 | $scope.fileType = asset.filetype && asset.filetype[0] === '.' 8 | ? asset.filetype.replace('.', '') 9 | : asset.filetype; 10 | 11 | // Clone so not to polute the original 12 | $scope.asset = _.cloneDeep(asset); 13 | 14 | $scope.acceptEdit = function() { 15 | if (!$scope.asset.name) { 16 | $scope.asset.name = $scope.originalName; 17 | } 18 | 19 | DataService.updateAsset($scope.asset) 20 | .then(function success(response) { 21 | $uibModalInstance.close(); 22 | }, function error(response) {}); 23 | }; 24 | 25 | $scope.deleteAsset = function() { 26 | DataService.deleteAsset(asset.id) 27 | .then(function success(response) { 28 | $uibModalInstance.close(); 29 | }, function error(response) {}); 30 | }; 31 | 32 | $scope.closeModal = function() { 33 | $uibModalInstance.dismiss(); 34 | }; 35 | } 36 | ]); 37 | -------------------------------------------------------------------------------- /assets/js/admin/edit-version-asset-modal/edit-version-asset-modal.pug: -------------------------------------------------------------------------------- 1 | form(editable-form='', name='assetForm', onaftersave='acceptEdit()', e-role='form') 2 | .modal-header 3 | h3.modal-title {{ asset.name }} 4 | .modal-body 5 | .form-horizontal 6 | // Version Name 7 | .form-group 8 | label.col-md-4.control-label(for='name') Asset Name 9 | .col-md-7 10 | span( 11 | editable-text='asset.name', 12 | e-name='name', 13 | e-placeholder='{{ originalName }}' 14 | ) 15 | | {{ asset.name }} 16 | // Select Platform 17 | .form-group 18 | label.col-md-4.control-label(for='platform') Platform 19 | .col-md-7 20 | span( 21 | editable-select='asset.platform', 22 | e-name='platform', 23 | e-ng-options='key as value for (key, value) in DataService.availablePlatforms', 24 | ) 25 | | {{ DataService.availablePlatforms[asset.platform] || 'Not set' }} 26 | // File Type 27 | .form-group(ng-show="fileType") 28 | label.col-md-4.control-label File Type 29 | .col-md-7 30 | | {{ fileType }} 31 | .modal-footer 32 | div(ng-if='!assetForm.$visible') 33 | button.btn.btn-warning(type='button', ng-click='assetForm.$show()') Edit 34 | button.btn.btn-danger(type='button', ng-click='deleteAsset()', confirm='Are you sure that you want to delete this asset?') Delete 35 | button.btn.btn-default(type='button', ng-click='closeModal()') Close 36 | div(ng-if='assetForm.$visible') 37 | button.btn.btn-warning(type='submit', ng-disable='assetForm.$waiting') Accept Changes 38 | button.btn.btn-danger(type='button', ng-click='assetForm.$cancel()') Cancel 39 | -------------------------------------------------------------------------------- /assets/js/admin/edit-version-modal/edit-version-modal-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.edit-version-modal', []) 2 | .controller('EditVersionModalController', ['$scope', 'DataService', 'Notification', '$uibModalInstance', '$uibModal', 'version', 'moment', 3 | ($scope, DataService, Notification, $uibModalInstance, $uibModal, version, moment) => { 4 | $scope.DataService = DataService; 5 | 6 | // Clone so not to polute the original 7 | $scope.version = _.cloneDeep(version); 8 | 9 | $scope.version.availability = new Date(version.availability); 10 | $scope.createdAt = new Date(version.createdAt); 11 | $scope.isAvailable = DataService.checkAvailability(version); 12 | 13 | /** 14 | * Updates the modal's knowlege of this version's assets from the one 15 | * maintained by DataService (which should be up to date with the server 16 | * because of SocketIO's awesomeness. 17 | */ 18 | var updateVersionAssets = function() { 19 | var updatedVersion = _.find(DataService.data, { 20 | id: version.id 21 | }); 22 | 23 | if (!updatedVersion) { 24 | // The version no longer exists 25 | return $uibModalInstance.close(); 26 | } 27 | 28 | $scope.version.assets = updatedVersion.assets; 29 | }; 30 | 31 | $scope.openAddAssetModal = function() { 32 | var modalInstance = $uibModal.open({ 33 | animation: true, 34 | templateUrl: 'js/admin/add-version-asset-modal/add-version-asset-modal.html', 35 | controller: 'AddVersionAssetModalController', 36 | resolve: { 37 | version: () => version 38 | } 39 | }); 40 | 41 | modalInstance.result.then(function() { 42 | // An asset should have been added, so we will update our modal's 43 | // knowledge of the version. 44 | updateVersionAssets(); 45 | }, function() {}); 46 | }; 47 | 48 | $scope.openEditAssetModal = function(asset) { 49 | var modalInstance = $uibModal.open({ 50 | animation: true, 51 | templateUrl: 'js/admin/edit-version-asset-modal/edit-version-asset-modal.html', 52 | controller: 'EditVersionAssetModalController', 53 | resolve: { 54 | asset: function() { 55 | return asset; 56 | } 57 | } 58 | }); 59 | 60 | modalInstance.result.then(function() { 61 | // An asset should have been modified or deleted, so we will update 62 | // our modal's knowledge of the version. 63 | updateVersionAssets(); 64 | }, function() {}); 65 | }; 66 | 67 | $scope.acceptEdit = function() { 68 | DataService.updateVersion($scope.version) 69 | .then(function success(response) { 70 | $uibModalInstance.close(); 71 | }, () => { 72 | if (!$scope.version.availability) { 73 | $scope.version.availability = new Date(version.availability); 74 | } 75 | }); 76 | }; 77 | 78 | $scope.makeAvailable = () => { 79 | const updatedVersion = { 80 | ...$scope.version, 81 | availability: moment().startOf('second').toDate() 82 | }; 83 | 84 | DataService 85 | .updateVersion(updatedVersion) 86 | .then($uibModalInstance.close, () => {}); 87 | }; 88 | 89 | $scope.deleteVersion = function() { 90 | DataService.deleteVersion(version.id) 91 | .then(function success(response) { 92 | $uibModalInstance.close(); 93 | }, function error(response) {}); 94 | }; 95 | 96 | $scope.closeModal = function() { 97 | $uibModalInstance.dismiss(); 98 | }; 99 | } 100 | ]); 101 | -------------------------------------------------------------------------------- /assets/js/admin/edit-version-modal/edit-version-modal.pug: -------------------------------------------------------------------------------- 1 | form(editable-form='', name='versionForm', onaftersave='acceptEdit()', e-role='form') 2 | .modal-header 3 | h3.modal-title {{ version.name }} 4 | .modal-body 5 | .form-horizontal 6 | // Version Name 7 | .form-group 8 | label.col-md-4.control-label Version Name 9 | .col-md-7 {{ version.name }} 10 | // Flavor Name 11 | .form-group 12 | label.col-md-4.control-label Flavor 13 | .col-md-7 {{ version.flavor.name }} 14 | // Select Channel 15 | .form-group 16 | label.col-md-4.control-label Channel 17 | .col-md-7 {{ version.channel.name || 'Not set' }} 18 | // Availability Date 19 | .form-group 20 | label.col-md-4.control-label(for='availability') Availability Date 21 | .col-md-7 22 | span( 23 | editable-text='version.availability', 24 | e-name='availability', 25 | e-type='datetime-local', 26 | e-min='{{ createdAt }}', 27 | e-step='1' 28 | ) 29 | p {{ version.availability | amDateFormat: 'YYYY-MM-DD, hh:mm:ss A' }} 30 | // Notes 31 | .form-group 32 | label.col-md-4.control-label(for='notes') Change Notes 33 | .col-md-7 34 | span( 35 | editable-textarea='version.notes', 36 | e-name='notes', 37 | e-rows='3', 38 | e-cols='60' 39 | ) 40 | p.release-notes {{ version.notes || 'Not set' }} 41 | 42 | h4 Assets 43 | table.table.table-bordered.table-hover.table-condensed.table-custom(ng-cloak) 44 | thead 45 | tr 46 | td Name 47 | td Platform 48 | td Downloads 49 | td 50 | tbody 51 | tr(data-ng-repeat='asset in version.assets') 52 | td {{ asset.name }} 53 | td {{ DataService.availablePlatforms[asset.platform] }} 54 | td {{ asset.download_count }} 55 | td(style="white-space: nowrap") 56 | button.btn.btn-default.btn-sm(type='button', ng-click="openEditAssetModal(asset)") 57 | | More 58 | button.btn.btn-success(type='button', ng-click='openAddAssetModal()') Add Asset 59 | 60 | div.text-left 61 | h4 Required Assets 62 | h5 63 | strong Windows 64 | p 65 | | New installs require 66 | code .exe 67 | | or 68 | code .msi 69 | | assets. Updates require 70 | code .nupkg 71 | | . 72 | h5 73 | strong OSX 74 | p 75 | | New installs require 76 | code .dmg 77 | | assets. Updates require 78 | code .zip 79 | | . 80 | h5 81 | strong Linux 82 | p 83 | | New installs require 84 | code .deb 85 | | , 86 | code .rpm 87 | | , 88 | code .tar.gz 89 | | or 90 | code .AppImage 91 | | assets. Updates are not supported. 92 | .modal-footer 93 | div(ng-if='!versionForm.$visible') 94 | button.btn.btn-warning.pull-left(ng-hide='isAvailable', type='button', ng-click='makeAvailable()') Make Available 95 | button.btn.btn-warning(type='button', ng-click='versionForm.$show()') Edit 96 | button.btn.btn-danger(type='button', ng-click='deleteVersion()', confirm='Are you sure that you want to delete this version?') Delete 97 | button.btn.btn-default(type='button', ng-click='closeModal()') Close 98 | div(ng-if='versionForm.$visible') 99 | button.btn.btn-warning(type='submit', ng-disable='versionForm.$waiting') Accept Changes 100 | button.btn.btn-danger(type='button', ng-click='versionForm.$cancel()') Cancel 101 | -------------------------------------------------------------------------------- /assets/js/admin/version-table/version-table-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.admin.version-table', []) 2 | .config(['$routeProvider', function($routeProvider) { 3 | $routeProvider 4 | .when('/admin', { 5 | templateUrl: 'js/admin/version-table/version-table.html', 6 | controller: 'AdminVersionTableController', 7 | data: { 8 | private: true 9 | } 10 | }); 11 | }]) 12 | .controller('AdminVersionTableController', ['$scope', 'Notification', 'DataService','$uibModal', 'PubSub', 13 | function($scope, Notification, DataService, $uibModal, PubSub) { 14 | $scope.flavor = 'all'; 15 | $scope.showAllFlavors = true; 16 | $scope.availableFlavors = DataService.availableFlavors && ['all', ...DataService.availableFlavors]; 17 | 18 | $scope.versions = DataService.data; 19 | $scope.hasMoreVersions = DataService.hasMore; 20 | 21 | $scope.filterVersionsByFlavor = () => { 22 | $scope.showAllFlavors = $scope.flavor === 'all'; 23 | $scope.versions = $scope.showAllFlavors 24 | ? DataService.data 25 | : DataService.data.filter(version => version.flavor.name === $scope.flavor); 26 | }; 27 | 28 | $scope.openEditModal = version => { 29 | var modalInstance = $uibModal.open({ 30 | animation: true, 31 | templateUrl: 'js/admin/edit-version-modal/edit-version-modal.html', 32 | controller: 'EditVersionModalController', 33 | resolve: { 34 | version: () => version 35 | } 36 | }); 37 | 38 | modalInstance.result.then(function() {}, function() {}); 39 | }; 40 | 41 | $scope.openAddVersionModal = function() { 42 | var modalInstance = $uibModal.open({ 43 | animation: true, 44 | templateUrl: 'js/admin/add-version-modal/add-version-modal.html', 45 | controller: 'AddVersionModalController' 46 | }); 47 | 48 | modalInstance.result.then(function() {}, function() {}); 49 | }; 50 | 51 | $scope.openAddFlavorModal = () => { 52 | const modalInstance = $uibModal.open({ 53 | animation: true, 54 | templateUrl: 'js/admin/add-flavor-modal/add-flavor-modal.html', 55 | controller: 'AddFlavorModalController' 56 | }); 57 | 58 | modalInstance.result.then(() => {}, () => {}); 59 | }; 60 | 61 | $scope.loadMoreVersions = function () { 62 | DataService.loadMoreVersions(); 63 | }; 64 | 65 | // Watch for changes to data content and update local data accordingly. 66 | var uid1 = PubSub.subscribe('data-change', function() { 67 | $scope.filterVersionsByFlavor(); 68 | $scope.hasMoreVersions = DataService.hasMore; 69 | $scope.availableFlavors = DataService.availableFlavors && ['all', ...DataService.availableFlavors]; 70 | }); 71 | 72 | $scope.$on('$destroy', function() { 73 | PubSub.unsubscribe(uid1); 74 | }); 75 | } 76 | ]); 77 | -------------------------------------------------------------------------------- /assets/js/admin/version-table/version-table.pug: -------------------------------------------------------------------------------- 1 | .row 2 | h3 Version Management 3 | small.pull-right 4 | | Flavor: 5 | select( 6 | ng-model='flavor' 7 | ng-options='flavor for flavor in availableFlavors' 8 | ng-change='filterVersionsByFlavor()' 9 | ) 10 | 11 | table.table.table-bordered.table-hover.table-condensed.table-custom(ng-if='versions.length' ng-cloak) 12 | thead 13 | tr 14 | td Name 15 | td Flavor 16 | td Channel 17 | td Asset Count 18 | td Date 19 | td Availability 20 | td 21 | tbody 22 | tr(data-ng-repeat='version in versions') 23 | td {{ version.name }} 24 | td {{ version.flavor.name }} 25 | td {{ version.channel.name }} 26 | td {{ version.assets.length }} 27 | td(am-time-ago="version.createdAt") 28 | td(am-time-ago="version.availability") 29 | td(style="white-space: nowrap") 30 | button.btn.btn-default.btn-sm(ng-click="openEditModal(version)") 31 | | More 32 | .jumbotron(ng-hide="versions.length") 33 | h5.text-gray(ng-show="showAllFlavors") No Versions Available 34 | h5.text-gray(ng-hide="showAllFlavors") The {{ flavor }} flavor has no versions available 35 | 36 | .btn-group 37 | a.btn.btn-default(ng-click="openAddVersionModal()") Add New Version 38 | a.btn.btn-default(ng-click="openAddFlavorModal()") Add New Flavor 39 | a.btn.btn-default(ng-show="hasMoreVersions" ng-click="loadMoreVersions()") Load more versions 40 | -------------------------------------------------------------------------------- /assets/js/core/auth/auth-service.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.auth.service', [ 2 | 'ngStorage' 3 | ]) 4 | .service('AuthService', ['$rootScope', 'AUTH_EVENTS', '$http', '$localStorage', '$q', 'jwtHelper', 5 | function($rootScope, AUTH_EVENTS, $http, $localStorage, $q, jwtHelper) { 6 | var self = this; 7 | 8 | self.getToken = function() { 9 | return $localStorage.token; 10 | }; 11 | 12 | self.login = function(credentials) { 13 | var defer = $q.defer(); 14 | $http 15 | .post('/api/auth/login', credentials) 16 | .then(function(res) { 17 | if (!res.data || !_.isString(res.data.token)) { 18 | $rootScope.$broadcast(AUTH_EVENTS.loginFailed); 19 | return defer.reject({ 20 | data: 'Expected a token in server response.' 21 | }); 22 | } 23 | 24 | var tokenContents = jwtHelper.decodeToken(res.data.token); 25 | if (!_.isObjectLike(tokenContents)) { 26 | $rootScope.$broadcast(AUTH_EVENTS.loginFailed); 27 | return defer.reject({ 28 | data: 'Invalid token received.' 29 | }); 30 | } 31 | 32 | $localStorage.token = res.data.token; 33 | $localStorage.tokenContents = tokenContents; 34 | $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); 35 | defer.resolve(true); 36 | }, function(err) { 37 | $rootScope.$broadcast(AUTH_EVENTS.loginFailed); 38 | defer.reject(err); 39 | }); 40 | 41 | return defer.promise; 42 | }; 43 | 44 | self.isAuthenticated = function() { 45 | var token = $localStorage.tokenContents; 46 | 47 | return (_.isObjectLike(token) && _.has(token, 'sub')); 48 | }; 49 | 50 | self.logout = function() { 51 | delete $localStorage.token; 52 | delete $localStorage.tokenContents; 53 | $rootScope.$broadcast(AUTH_EVENTS.logoutSuccess); 54 | }; 55 | } 56 | ]); 57 | -------------------------------------------------------------------------------- /assets/js/core/auth/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.auth', [ 2 | 'app.core.auth.service', 3 | 'app.core.auth.login', 4 | 'app.core.auth.logout', 5 | 'angular-jwt' 6 | ]).constant('AUTH_EVENTS', { 7 | loginSuccess: 'auth-login-success', 8 | loginFailed: 'auth-login-failed', 9 | logoutSuccess: 'auth-logout-success', 10 | sessionTimeout: 'auth-session-timeout', 11 | notAuthenticated: 'auth-not-authenticated', 12 | notAuthorized: 'auth-not-authorized' 13 | }) 14 | .directive('authToolbar', function() { 15 | return { 16 | restrict: 'E', 17 | templateUrl: '/templates/auth-toolbar.html' 18 | }; 19 | }) 20 | .config(['$httpProvider', 'jwtInterceptorProvider', 21 | function($httpProvider, jwtInterceptorProvider) { 22 | // Please note we're annotating the function so that the $injector works when the file is minified 23 | jwtInterceptorProvider.tokenGetter = ['AuthService', function(AuthService) { 24 | return AuthService.getToken(); 25 | }]; 26 | 27 | $httpProvider.interceptors.push('jwtInterceptor'); 28 | } 29 | ]) 30 | .run(['$rootScope', 'AUTH_EVENTS', 'AuthService', 'Notification', '$location', 31 | function($rootScope, AUTH_EVENTS, AuthService, Notification, $location) { 32 | $rootScope.$on('$routeChangeStart', function(event, next) { 33 | // Consider whether to redirect the request if it is unauthorized 34 | if ( 35 | next.data && 36 | next.data.private 37 | ) { 38 | if (!AuthService.isAuthenticated()) { 39 | console.log('Unauthorized request, redirecting...'); 40 | event.preventDefault(); 41 | // User is not logged in 42 | $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); 43 | 44 | Notification.error({ 45 | title: 'Unauthorized', 46 | message: 'Please login' 47 | }); 48 | 49 | // Redirect the user to the login page 50 | $location.path('/auth/login'); 51 | } 52 | } 53 | }); 54 | } 55 | ]); 56 | -------------------------------------------------------------------------------- /assets/js/core/auth/login/login-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.auth.login', [ 2 | 'ngStorage', 3 | 'ngRoute' 4 | ]) 5 | .config(['$routeProvider', function($routeProvider) { 6 | $routeProvider 7 | .when('/auth/login', { 8 | templateUrl: 'js/core/auth/login/login.html', 9 | controller: 'LoginController', 10 | data: { 11 | private: false 12 | } 13 | }); 14 | }]) 15 | .controller('LoginController', ['$scope', 'Notification', 'AuthService', '$location', 16 | function($scope, Notification, AuthService, $location) { 17 | if (AuthService.isAuthenticated()) { 18 | $location.path('/admin'); 19 | } 20 | 21 | $scope.credentials = { 22 | username: '', 23 | password: '' 24 | }; 25 | 26 | $scope.login = function(credentials) { 27 | AuthService.login(credentials).then(function(user) { 28 | Notification.success({ 29 | message: 'Login Successful' 30 | }); 31 | 32 | $location.path('/admin'); 33 | }, function(err) { 34 | const notificationObject = { 35 | title: err.statusText || 'Unable to login', 36 | message: err.data || 'Invalid Credentials' 37 | }; 38 | 39 | Notification.error(notificationObject); 40 | }); 41 | }; 42 | } 43 | ]); 44 | -------------------------------------------------------------------------------- /assets/js/core/auth/login/login.pug: -------------------------------------------------------------------------------- 1 | .row 2 | form.form.form-horizontal(name='login-form', ng-submit='login(credentials)') 3 | fieldset 4 | // Form Name 5 | .page-header Login 6 | // Text input 7 | .form-group 8 | label.col-md-4.control-label(for='username') Username 9 | .col-md-6 10 | input.form-control.input-md(name='username', type='text', placeholder='Username',ng-model='credentials.username' required) 11 | span.help-block Your LDAP credentials work here 12 | // Password input 13 | .form-group 14 | label.col-md-4.control-label(for='password') Password 15 | .col-md-6 16 | input.form-control.input-md(name='password', type='password', placeholder='Password', ng-model='credentials.password' required) 17 | // Button (Double) 18 | .form-group 19 | .col-md-4 20 | .col-md-8 21 | input.btn.btn-success(type='submit', value='Login') 22 | a.btn.btn-warning(href='/') Cancel 23 | -------------------------------------------------------------------------------- /assets/js/core/auth/logout/logout-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.auth.logout', [ 2 | ]) 3 | .config(['$routeProvider', function($routeProvider) { 4 | $routeProvider 5 | .when('/auth/logout', { 6 | templateUrl: 'js/core/auth/logout/logout.html', 7 | controller: 'LogoutController', 8 | data: { 9 | private: false 10 | } 11 | }); 12 | }]) 13 | .controller('LogoutController', ['Notification', 'AuthService', '$location', 14 | function(Notification, AuthService, $location) { 15 | AuthService.logout(); 16 | 17 | Notification.success({ 18 | message: 'Logout Successful' 19 | }); 20 | 21 | $location.path('/auth/login'); 22 | } 23 | ]); 24 | -------------------------------------------------------------------------------- /assets/js/core/auth/logout/logout.pug: -------------------------------------------------------------------------------- 1 | .row 2 | h3.text-center.text-gray Logging out, please wait... 3 | -------------------------------------------------------------------------------- /assets/js/core/core.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core', [ 2 | 'app.core.dependencies', 3 | 'app.core.auth', 4 | 'app.core.data', 5 | 'app.core.nav' 6 | ]); 7 | -------------------------------------------------------------------------------- /assets/js/core/data/data.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.data', [ 2 | 'app.core.data.service' 3 | ]) 4 | .run(['DataService', 5 | function(DataService) { 6 | DataService.initialize(); 7 | } 8 | ]); 9 | -------------------------------------------------------------------------------- /assets/js/core/dependencies/dependencies.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.dependencies', [ 2 | 'ngRoute', 3 | 'ngAnimate', 4 | 'ngMessages', 5 | 'ngSanitize', 6 | 'ngFileUpload', 7 | 'ui-notification', 8 | 'ui.bootstrap', 9 | 'ng.deviceDetector', 10 | 'angular-confirm', 11 | 'PubSub', 12 | 'xeditable', 13 | 'angularMoment' 14 | ]); 15 | -------------------------------------------------------------------------------- /assets/js/core/nav-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.core.nav', []) 2 | .controller('NavController', ['$scope', 'Session', function($scope, Session) { 3 | $scope.Session = Session; 4 | $scope.shouldHideNavMobile = true; 5 | $scope.toggleNavMobile = function() { 6 | $scope.shouldHideNavMobile = !$scope.shouldHideNavMobile; 7 | }; 8 | $scope.hideNavMobile = function() { 9 | $scope.shouldHideNavMobile = true; 10 | }; 11 | }]); 12 | -------------------------------------------------------------------------------- /assets/js/download/download-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.releases', []) 2 | .config(['$routeProvider', function($routeProvider) { 3 | $routeProvider 4 | .when('/releases/:channel?/:flavor?', { 5 | templateUrl: 'js/download/download.html', 6 | controller: 'DownloadController as vm' 7 | }); 8 | }]) 9 | .controller('DownloadController', [ 10 | '$scope', '$routeParams', '$route', 'PubSub', 'deviceDetector', 11 | 'DataService', 12 | function( 13 | $scope, $routeParams, $route, PubSub, deviceDetector, DataService 14 | ) { 15 | var self = this; 16 | self.showAllVersions = false; 17 | $scope.hasMoreVersions = DataService.hasMore; 18 | 19 | self.platform = deviceDetector.os; 20 | if (self.platform === 'mac') { 21 | self.platform = 'osx'; 22 | self.archs = ['64', 'arm64']; 23 | } else { 24 | self.archs = ['32', '64']; 25 | } 26 | 27 | self.setRouteParams = (channel, flavor) => { 28 | $route.updateParams({ 29 | channel, 30 | flavor 31 | }); 32 | }; 33 | 34 | self.availablePlatforms = DataService.availablePlatforms; 35 | self.filetypes = DataService.filetypes; 36 | self.availableChannels = DataService.availableChannels; 37 | self.availableFlavors = DataService.availableFlavors; 38 | 39 | // Get selected channel from route or set to default (stable) 40 | self.channel = $routeParams.channel || (self.availableChannels && self.availableChannels[0]) || 'stable'; 41 | 42 | // Get selected flavor from route or set to default (default) 43 | self.flavor = $routeParams.flavor || 'default'; 44 | 45 | self.latestReleases = null; 46 | self.downloadUrl = null; 47 | 48 | self.getLatestReleases = function() { 49 | self.setRouteParams( 50 | self.channel, 51 | self.flavor 52 | ); 53 | self.latestReleases = DataService.getLatestReleases( 54 | self.platform, 55 | self.archs, 56 | self.channel, 57 | self.flavor 58 | ); 59 | self.versions = DataService.data 60 | .filter(DataService.checkAvailability) 61 | .filter(version => version.flavor.name === self.flavor); 62 | }; 63 | 64 | // Watch for changes to data content and update local data accordingly. 65 | var uid1 = PubSub.subscribe('data-change', function() { 66 | self.getLatestReleases(); 67 | self.availableChannels = DataService.availableChannels; 68 | self.availableFlavors = DataService.availableFlavors; 69 | $scope.hasMoreVersions = DataService.hasMore; 70 | }); 71 | 72 | // Update knowledge of the latest available versions. 73 | self.getLatestReleases(); 74 | 75 | self.download = function(asset, versionName, flavorName) { 76 | if (!asset) { 77 | return; 78 | } 79 | 80 | const { flavor, version, platform, name } = asset; 81 | 82 | self.downloadUrl = 83 | `/download/flavor/${flavorName || flavor}/${versionName || version}/${platform}/${name}`; 84 | }; 85 | 86 | $scope.$on('$destroy', function() { 87 | PubSub.unsubscribe(uid1); 88 | }); 89 | 90 | $scope.loadMoreVersions = function () { 91 | DataService.loadMoreVersions(); 92 | }; 93 | } 94 | ]); 95 | -------------------------------------------------------------------------------- /assets/js/home/home-controller.js: -------------------------------------------------------------------------------- 1 | angular.module('app.home', []) 2 | .config(['$routeProvider', function($routeProvider) { 3 | $routeProvider 4 | .when('/home', { 5 | templateUrl: 'js/home/home.html', 6 | controller: 'HomeController as vm' 7 | }); 8 | }]) 9 | .controller('HomeController', ['$scope', 'PubSub', 'DataService', 10 | function($scope, PubSub, DataService) { 11 | } 12 | ]); 13 | -------------------------------------------------------------------------------- /assets/js/home/home.pug: -------------------------------------------------------------------------------- 1 | .row.text-center 2 | .col-md-12 3 | h2 #{process.env.WEBSITE_TITLE || 'Our Amazing Electron App'} 4 | 5 | if process.env.WEBSITE_HOME_CONTENT 6 | div !{process.env.WEBSITE_HOME_CONTENT} 7 | else 8 | .row.text-center.padding-20-0 9 | .col-md-4 10 | i.fa.fa-5x.fa-diamond 11 | h3 Polished 12 | .col-md-4 13 | i.fa.fa-5x.fa-magic 14 | h3 Magic 15 | .col-md-4 16 | i.fa.fa-5x.fa-bomb 17 | h3 TheBomb.com 18 | 19 | .row.text-center 20 | .col-md-12 21 | .jumbotron 22 | p 23 | a.btn.btn-success.btn-lg(href="/releases") 24 | | Get it now 25 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | angular.module('app', [ 2 | 'app.core', 3 | 'app.admin', 4 | 'app.home', 5 | 'app.releases' 6 | ]) 7 | .config(['$routeProvider', '$locationProvider', 'NotificationProvider', 8 | function($routeProvider, $locationProvider, NotificationProvider) { 9 | $routeProvider.otherwise({ 10 | redirectTo: '/home' 11 | }); 12 | 13 | // Use the HTML5 History API 14 | $locationProvider.html5Mode(true); 15 | 16 | NotificationProvider.setOptions({ 17 | positionX: 'left', 18 | positionY: 'bottom' 19 | }); 20 | } 21 | ]) 22 | .run(['editableOptions', 'editableThemes', 23 | function(editableOptions, editableThemes) { 24 | editableThemes.bs3.inputClass = 'input-sm'; 25 | editableThemes.bs3.buttonsClass = 'btn-sm'; 26 | editableThemes.bs3.controlsTpl = '
'; 27 | editableOptions.theme = 'bs3'; // bootstrap3 theme. Can be also 'bs2', 'default' 28 | } 29 | ]) 30 | .controller('MainController', ['$scope', 'AuthService', 31 | function($scope, AuthService) { 32 | $scope.isAuthenticated = AuthService.isAuthenticated; 33 | } 34 | ]); 35 | -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | # The robots.txt file is used to control how search engines index your live URLs. 2 | # See http://www.robotstxt.org/wc/norobots.html for more information. 3 | 4 | 5 | 6 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 7 | # User-Agent: * 8 | # Disallow: / 9 | -------------------------------------------------------------------------------- /assets/styles/_custom.scss: -------------------------------------------------------------------------------- 1 | .text-gray { 2 | color: $gray-light; 3 | } 4 | 5 | .text-capitalize { 6 | text-transform: capitalize; 7 | } 8 | 9 | .text-color-lock { 10 | color: darken(desaturate($brand-primary, 50), 10); 11 | } 12 | 13 | .pointer { 14 | cursor: pointer; 15 | } 16 | 17 | .release-notes { 18 | white-space: pre; 19 | font-size: 13px !important; 20 | } 21 | 22 | .timestamp { 23 | font-size: 13px !important; 24 | } 25 | 26 | // Input label position 27 | .form-horizontal .control-label { 28 | padding-top: 0; 29 | } 30 | 31 | .padding-20-0 { 32 | padding: 20px 0; 33 | } 34 | 35 | .padding-top-20 { 36 | padding-top: 20px; 37 | } 38 | 39 | // Input status colours 40 | .form-control, 41 | input, 42 | textarea { 43 | border-style: solid; 44 | } 45 | 46 | .has-success { 47 | .form-control, 48 | .form-control:focus { 49 | border-color: $brand-success; 50 | color: $brand-success; 51 | } 52 | } 53 | 54 | .is-warning { 55 | color: $brand-warning; 56 | } 57 | 58 | .has-warning { 59 | .form-control, 60 | .form-control:focus { 61 | border-color: $brand-warning; 62 | color: $brand-warning; 63 | } 64 | } 65 | 66 | .has-error { 67 | .form-control, 68 | .form-control:focus { 69 | border-color: $brand-danger; 70 | color: $brand-danger; 71 | } 72 | } 73 | // Fix radio & checkbox input positioning 74 | .radio input[type="radio"], 75 | .radio-inline input[type="radio"], 76 | .checkbox input[type="checkbox"], 77 | .checkbox-inline input[type="checkbox"] { 78 | margin-left: 0; 79 | } 80 | // Navbar Logo 81 | .navbar-logo { 82 | padding: 10px 15px; 83 | height: 60px; 84 | } 85 | 86 | .browsehappy { 87 | background: #ccc; 88 | color: #000; 89 | margin: 0.2em 0; 90 | padding: 0.2em 0; 91 | } 92 | // Custom page footer 93 | .footer { 94 | border-top: 1px solid #333; 95 | color: #777; 96 | margin: 30px 0; 97 | padding-top: 10px; 98 | } 99 | // Panel header button 100 | .modal-header-button { 101 | margin-right: -16px; 102 | margin-top: -9px; 103 | } 104 | // Modal footnote 105 | .modal-footer-note { 106 | float: left; 107 | font-style: italic; 108 | height: 45px; 109 | line-height: 45px; 110 | text-align: left; 111 | 112 | .progress { 113 | height: 45px; 114 | 115 | .progress-bar { 116 | font-size: 14px; 117 | line-height: 45px; 118 | } 119 | } 120 | } 121 | // Editable table 122 | .table-custom { 123 | td { 124 | vertical-align: middle !important; 125 | text-align: center; 126 | } 127 | } 128 | 129 | .editable-wrap { 130 | display: inline-block; 131 | margin: 0; 132 | white-space: nowrap; 133 | width: 100%; 134 | 135 | .editable-controls, 136 | .editable-error { 137 | margin-bottom: 4px; 138 | } 139 | 140 | .editable-controls { 141 | & > input, 142 | & > select, 143 | & > textarea { 144 | margin-bottom: 0; 145 | } 146 | } 147 | 148 | .editable-input { 149 | display: inline-block; 150 | } 151 | } 152 | 153 | .editable-buttons { 154 | display: inline-block; 155 | vertical-align: top; 156 | 157 | button { 158 | margin-left: 5px; 159 | } 160 | } 161 | 162 | .editable-input.editable-has-buttons { 163 | width: auto; 164 | } 165 | 166 | .editable-bstime { 167 | .editable-input input[type=text] { 168 | width: 46px; 169 | } 170 | 171 | .well-small { 172 | margin-bottom: 0; 173 | padding: 10px; 174 | } 175 | } 176 | 177 | .editable-range output { 178 | display: inline-block; 179 | min-width: 30px; 180 | text-align: center; 181 | vertical-align: top; 182 | } 183 | 184 | .editable-color input[type=color] { 185 | width: 50px; 186 | } 187 | 188 | .editable-checkbox label span, 189 | .editable-checklist label span, 190 | .editable-radiolist label span { 191 | margin-left: 7px; 192 | margin-right: 10px; 193 | } 194 | 195 | .editable-hide { 196 | display: none !important; 197 | } 198 | 199 | .editable-click, 200 | a.editable-click { 201 | border-bottom: dashed 1px $brand-info; 202 | color: $brand-info; 203 | text-decoration: none; 204 | } 205 | 206 | .editable-click:hover, 207 | a.editable-click:hover { 208 | border-bottom-color: darken($brand-info, 15); 209 | color: darken($brand-info, 15); 210 | text-decoration: none; 211 | } 212 | 213 | .editable-empty, 214 | .editable-empty:hover, 215 | .editable-empty:focus, 216 | a.editable-empty, 217 | a.editable-empty:hover, 218 | a.editable-empty:focus, 219 | .empty-cell { 220 | color: $brand-danger; 221 | font-style: italic; 222 | text-decoration: none; 223 | } 224 | 225 | input[type="number"].ng-valid { 226 | border-color: green; 227 | color: green; 228 | } 229 | 230 | input[type="number"].ng-invalid { 231 | border-color: red; 232 | color: red; 233 | } 234 | 235 | .download-btn { 236 | margin-right: 10px; 237 | margin-bottom: 10px; 238 | } 239 | 240 | .btn-group .btn { 241 | margin-right: 5px; 242 | } 243 | -------------------------------------------------------------------------------- /assets/styles/_variable_override.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/'; 2 | 3 | // Override the default flatly brand colour 4 | $brand-primary: #088dd9 !default; 5 | $brand-danger: $brand-primary !default; 6 | $navbar-default-link-hover-color: #ccc !default; 7 | $border-radius-base: 0 !default; 8 | $border-radius-large: 0 !default; 9 | $border-radius-small: 0 !default; 10 | -------------------------------------------------------------------------------- /assets/styles/importer.scss: -------------------------------------------------------------------------------- 1 | @import '_variable_override.scss'; 2 | @import '_variables.scss'; 3 | // bower:scss 4 | @import '../bower_components/bootstrap-sass-official/assets/stylesheets/_bootstrap.scss'; 5 | // endbower 6 | @import '_bootswatch.scss'; 7 | @import '_custom.scss'; 8 | -------------------------------------------------------------------------------- /assets/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArekSredzki/electron-release-server/042567b37419ed2d74c740128b8f870baa5e86bc/assets/templates/.gitkeep -------------------------------------------------------------------------------- /assets/templates/auth-toolbar.pug: -------------------------------------------------------------------------------- 1 | span.pull-right(ng-cloak) 2 | ul.nav.navbar-nav 3 | li(ng-class="{ active: isActive('/admin')}") 4 | a(ng-href='/admin') 5 | i.fa.fa-lock.fa-fw.text-color-lock(ng-show="!isAuthenticated()") 6 | i.fa.fa-unlock-alt.fa-fw.text-color-lock(ng-show="isAuthenticated()") 7 | | Admin 8 | span(ng-show="isAuthenticated()") 9 | a.btn.btn-default.navbar-btn(href="auth/logout") 10 | | Logout 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-release-server", 3 | "private": true, 4 | "dependencies": { 5 | "angular": "1.5.9", 6 | "bootstrap-sass-official": "~3.3.7", 7 | "angular-animate": "1.5.9", 8 | "angular-messages": "~1.5.9", 9 | "angular-route": "~1.5.9", 10 | "angular-sanitize": "~1.5.9", 11 | "angular-xeditable": "~0.5.0", 12 | "lodash": "~4.17.2", 13 | "angular-ui-notification": "~0.2.0", 14 | "angular-bootstrap": "~1.3.3", 15 | "angular-confirm-modal": "~1.2.6", 16 | "angular-jwt": "~0.1.8", 17 | "ngstorage": "~0.3.11", 18 | "sails.io.js": "^1.2.1", 19 | "angular-sails": "~1.1.4", 20 | "angular-moment": "~1.0.1", 21 | "components-font-awesome": "~4.7.0", 22 | "ng-file-upload": "~12.2.13", 23 | "angular-PubSub": "angular.pubsub#*", 24 | "ng-device-detector": "~3.0.1", 25 | "compare-versions": "~3.0.0" 26 | }, 27 | "appPath": "app", 28 | "overrides": { 29 | "angular-xeditable": { 30 | "main": [ 31 | "dist/js/xeditable.min.js" 32 | ] 33 | }, 34 | "bootstrap-sass-official": { 35 | "main": [ 36 | "assets/stylesheets/_bootstrap.scss" 37 | ] 38 | } 39 | }, 40 | "resolutions": { 41 | "angular": "~1.5.9" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap 3 | * (sails.config.bootstrap) 4 | * 5 | * An asynchronous bootstrap function that runs before your Sails app gets lifted. 6 | * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. 7 | * 8 | * For more information on bootstrapping your app, check out: 9 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.bootstrap.html 10 | */ 11 | 12 | const mapSeries = require('async/mapSeries'); 13 | const waterfall = require('async/waterfall'); 14 | const series = require('async/series'); 15 | 16 | module.exports.bootstrap = done => { 17 | series([ 18 | 19 | // Create configured channels in database 20 | cb => mapSeries(sails.config.channels, (name, next) => { 21 | waterfall([ 22 | next => { 23 | Channel 24 | .find(name) 25 | .exec(next); 26 | }, 27 | (result, next) => { 28 | if (result.length) return next(); 29 | 30 | Channel 31 | .create({ name }) 32 | .exec(next); 33 | } 34 | ], next); 35 | }, cb), 36 | 37 | // Populate existing versions without availability date using version creation date 38 | cb => Version 39 | .find({ availability: null }) 40 | .then(versions => mapSeries( 41 | versions, 42 | ({ id, createdAt }, next) => { 43 | Version 44 | .update(id, { availability: createdAt }) 45 | .exec(next) 46 | }, 47 | cb 48 | )), 49 | 50 | // Create configured flavors in database 51 | cb => mapSeries(sails.config.flavors, (name, next) => { 52 | waterfall([ 53 | next => { 54 | Flavor 55 | .find(name) 56 | .exec(next); 57 | }, 58 | (result, next) => { 59 | if (result.length) return next(); 60 | 61 | Flavor 62 | .create({ name }) 63 | .exec(next); 64 | } 65 | ], next); 66 | }, cb), 67 | 68 | // Update existing versions and associated assets in database with default flavor data 69 | cb => Version 70 | .update( 71 | { flavor: null }, 72 | { flavor: 'default' } 73 | ) 74 | .exec((err, updatedVersions) => mapSeries( 75 | updatedVersions, 76 | ({ name, id }, next) => { 77 | Asset 78 | .update( 79 | { version: name }, 80 | { version: id } 81 | ) 82 | .exec(next) 83 | }, 84 | cb 85 | )) 86 | 87 | ], done); 88 | }; 89 | -------------------------------------------------------------------------------- /config/channels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Available release channels 3 | * 4 | * These channels will be created by the app on startup. 5 | * 6 | * Ordered by descending stability. 7 | */ 8 | module.exports.channels = [ 9 | 'stable', 10 | 'rc', 11 | 'beta', 12 | 'alpha' 13 | ]; 14 | -------------------------------------------------------------------------------- /config/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Site Request Forgery Protection Settings 3 | * (sails.config.csrf) 4 | * 5 | * CSRF tokens are like a tracking chip. While a session tells the server that a user 6 | * "is who they say they are", a csrf token tells the server "you are where you say you are". 7 | * 8 | * When enabled, all non-GET requests to the Sails server must be accompanied by 9 | * a special token, identified as the '_csrf' parameter. 10 | * 11 | * This option protects your Sails app against cross-site request forgery (or CSRF) attacks. 12 | * A would-be attacker needs not only a user's session cookie, but also this timestamped, 13 | * secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. 14 | * 15 | * This allows us to have certainty that our users' requests haven't been hijacked, 16 | * and that the requests they're making are intentional and legitimate. 17 | * 18 | * This token has a short-lived expiration timeline, and must be acquired by either: 19 | * 20 | * (a) For traditional view-driven web apps: 21 | * Fetching it from one of your views, where it may be accessed as 22 | * a local variable, e.g.: 23 | * 26 | * 27 | * or (b) For AJAX/Socket-heavy and/or single-page apps: 28 | * Sending a GET request to the `/csrfToken` route, where it will be returned 29 | * as JSON, e.g.: 30 | * { _csrf: 'ajg4JD(JGdajhLJALHDa' } 31 | * 32 | * 33 | * Enabling this option requires managing the token in your front-end app. 34 | * For traditional web apps, it's as easy as passing the data from a view into a form action. 35 | * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. 36 | * 37 | * For more information on CSRF, check out: 38 | * http://en.wikipedia.org/wiki/Cross-site_request_forgery 39 | * 40 | * For more information on this configuration file, including info on CSRF + CORS, see: 41 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.csrf.html 42 | * 43 | */ 44 | 45 | /**************************************************************************** 46 | * * 47 | * Enabled CSRF protection for your site? * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // module.exports.csrf = false; 52 | 53 | /**************************************************************************** 54 | * * 55 | * You may also specify more fine-grained settings for CSRF, including the * 56 | * domains which are allowed to request the CSRF token via AJAX. These * 57 | * settings override the general CORS settings in your config/cors.js file. * 58 | * * 59 | ****************************************************************************/ 60 | 61 | // module.exports.csrf = { 62 | // grantTokenViaAjax: true, 63 | // origin: '' 64 | // } 65 | -------------------------------------------------------------------------------- /config/datastores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS FILE WAS ADDED AUTOMATICALLY by the Sails 1.0 app migration tool. 3 | */ 4 | 5 | module.exports.datastores = { 6 | 7 | // In previous versions, datastores (then called 'connections') would only be loaded 8 | // if a model was actually using them. Starting with Sails 1.0, _all_ configured 9 | // datastores will be loaded, regardless of use. So we'll only include datastores in 10 | // this file that were actually being used. Your original `connections` config is 11 | // still available as `config/connections-old.js.txt`. 12 | 13 | 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /config/docker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docker environment settings 3 | */ 4 | 5 | module.exports = { 6 | 7 | models: { 8 | datastore: 'postgresql', 9 | migrate: 'alter', 10 | dataEncryptionKeys: { 11 | // DEKs should be 32 bytes long, and cryptographically random. 12 | // You can generate such a key by running the following: 13 | // require('crypto').randomBytes(32).toString('base64') 14 | default: process.env['DATA_ENCRYPTION_KEY'], 15 | } 16 | }, 17 | 18 | port: 80, 19 | 20 | log: { 21 | level: process.env['LOG_LEVEL'] 22 | }, 23 | 24 | auth: { 25 | static: { 26 | username: process.env['APP_USERNAME'], 27 | password: process.env['APP_PASSWORD'] 28 | } 29 | }, 30 | appUrl: process.env['APP_URL'], 31 | datastores: { 32 | postgresql: { 33 | adapter: 'sails-postgresql', 34 | host: process.env['DB_HOST'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[1].split(':')[0], 35 | port: process.env['DB_PORT'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[1].split(':')[1].split('/')[0], 36 | user: process.env['DB_USERNAME'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[0].split(':')[1].split('/')[2], 37 | password: process.env['DB_PASSWORD'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[0].split(':')[2], 38 | database: process.env['DB_NAME'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('/')[3] 39 | } 40 | }, 41 | jwt: { 42 | // Recommended: 63 random alpha-numeric characters 43 | // Generate using: https://www.grc.com/passwords.htm 44 | token_secret: process.env['TOKEN_SECRET'], 45 | }, 46 | files: { 47 | dirname: process.env['ASSETS_PATH'] || '/tmp/', 48 | }, 49 | session: { 50 | // Recommended: 63 random alpha-numeric characters 51 | // Generate using: https://www.grc.com/passwords.htm 52 | secret: process.env['TOKEN_SECRET'], 53 | database: process.env['DB_NAME'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('/')[3], 54 | host: process.env['DB_HOST'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[1].split(':')[0], 55 | user: process.env['DB_USERNAME'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[0].split(':')[1].split('/')[2], 56 | password: process.env['DB_PASSWORD'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[0].split(':')[2], 57 | port: process.env['DB_PORT'] || process.env['DATABASE_URL'] && process.env['DATABASE_URL'].split('@')[1].split(':')[1].split('/')[0] 58 | } 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Development environment settings 3 | * 4 | * This file can include shared settings for a development team, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the development * 17 | * environment (see config/datastores.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | models: { 21 | datastore: 'postgresql' 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Production environment settings 3 | * 4 | * This file can include shared settings for a production environment, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the production * 17 | * environment (see config/datastores.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | models: { 21 | datastore: 'postgresql', 22 | migrate: 'safe' 23 | }, 24 | 25 | /*************************************************************************** 26 | * Set the port in the production environment to 80 * 27 | ***************************************************************************/ 28 | 29 | port: 5014, 30 | 31 | /*************************************************************************** 32 | * Set the log level in production environment to "silent" * 33 | ***************************************************************************/ 34 | 35 | // log: { 36 | // level: "silent" 37 | // } 38 | 39 | // auth: { 40 | // secret: 'temppass' 41 | // } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /config/files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File options 3 | * Options which relate to filesystem storage of assets 4 | */ 5 | module.exports.files = { 6 | // Maximum allowed file size in bytes 7 | // Defaults to 500MB 8 | maxBytes: 524288000, 9 | // The fs directory name at which files will be kept 10 | dirname: '' 11 | }; 12 | -------------------------------------------------------------------------------- /config/flavors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Available release flavors 3 | * 4 | * These flavors will be created by the app on startup. 5 | */ 6 | module.exports.flavors = [ 7 | 'default' 8 | ]; 9 | -------------------------------------------------------------------------------- /config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Variable Configuration 3 | * (sails.config.globals) 4 | * 5 | * Configure which global variables which will be exposed 6 | * automatically by Sails. 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.globals.html 10 | */ 11 | module.exports.globals = { 12 | 13 | /**************************************************************************** 14 | * * 15 | * Expose the lodash installed in Sails core as a global variable. If this * 16 | * is disabled, like any other node module you can always run npm install * 17 | * lodash --save, then var _ = require('lodash') at the top of any file. * 18 | * * 19 | ****************************************************************************/ 20 | 21 | _: require('lodash'), 22 | 23 | /**************************************************************************** 24 | * * 25 | * Expose the async installed in Sails core as a global variable. If this is * 26 | * disabled, like any other node module you can always run npm install async * 27 | * --save, then var async = require('async') at the top of any file. * 28 | * * 29 | ****************************************************************************/ 30 | 31 | async: require('async'), 32 | 33 | /**************************************************************************** 34 | * * 35 | * Expose the sails instance representing your app. If this is disabled, you * 36 | * can still get access via req._sails. * 37 | * * 38 | ****************************************************************************/ 39 | 40 | sails: true, 41 | 42 | /**************************************************************************** 43 | * * 44 | * Expose each of your app's services as global variables (using their * 45 | * "globalId"). E.g. a service defined in api/models/NaturalLanguage.js * 46 | * would have a globalId of NaturalLanguage by default. If this is disabled, * 47 | * you can still access your services via sails.services.* * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // services: true, 52 | 53 | /**************************************************************************** 54 | * * 55 | * Expose each of your app's models as global variables (using their * 56 | * "globalId"). E.g. a model defined in api/models/User.js would have a * 57 | * globalId of User by default. If this is disabled, you can still access * 58 | * your models via sails.models.*. * 59 | * * 60 | ****************************************************************************/ 61 | 62 | models: true 63 | }; 64 | -------------------------------------------------------------------------------- /config/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Server Settings 3 | * (sails.config.http) 4 | * 5 | * Configuration for the underlying HTTP server in Sails. 6 | * Only applies to HTTP requests (not WebSockets) 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.http.html 10 | */ 11 | 12 | module.exports.http = { 13 | 14 | /**************************************************************************** 15 | * * 16 | * Express middleware to use for every Sails request. To add custom * 17 | * middleware to the mix, add a function to the middleware config object and * 18 | * add its key to the "order" array. The $custom key is reserved for * 19 | * backwards-compatibility with Sails v0.9.x apps that use the * 20 | * `customMiddleware` config option. * 21 | * * 22 | ****************************************************************************/ 23 | 24 | // middleware: { 25 | 26 | /*************************************************************************** 27 | * * 28 | * The order in which middleware should be run for HTTP request. (the Sails * 29 | * router is invoked by the "router" middleware below.) * 30 | * * 31 | ***************************************************************************/ 32 | 33 | // order: [ 34 | // 'startRequestTimer', 35 | // 'cookieParser', 36 | // 'session', 37 | // 'myRequestLogger', 38 | // 'bodyParser', 39 | // 'handleBodyParserError', 40 | // 'compress', 41 | // 'methodOverride', 42 | // 'poweredBy', 43 | // '$custom', 44 | // 'router', 45 | // 'www', 46 | // 'favicon', 47 | // '404', 48 | // '500' 49 | // ], 50 | 51 | /**************************************************************************** 52 | * * 53 | * Example custom middleware; logs each request to the console. * 54 | * * 55 | ****************************************************************************/ 56 | 57 | // myRequestLogger: function (req, res, next) { 58 | // console.log("Requested :: ", req.method, req.url); 59 | // return next(); 60 | // } 61 | 62 | 63 | /*************************************************************************** 64 | * * 65 | * The body parser that will handle incoming multipart HTTP requests. By * 66 | * default as of v0.10, Sails uses * 67 | * [skipper](http://github.com/balderdashy/skipper). See * 68 | * http://www.senchalabs.org/connect/multipart.html for other options. * 69 | * * 70 | ***************************************************************************/ 71 | 72 | // bodyParser: require('skipper') 73 | 74 | // }, 75 | 76 | /*************************************************************************** 77 | * * 78 | * The number of seconds to cache flat files on disk being served by * 79 | * Express static middleware (by default, these files are in `.tmp/public`) * 80 | * * 81 | * The HTTP static cache is only active in a 'production' environment, * 82 | * since that's the only time Express will cache flat-files. * 83 | * * 84 | ***************************************************************************/ 85 | 86 | cache: 30 87 | }; 88 | -------------------------------------------------------------------------------- /config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (sails.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * 9 | * For more informationom i18n in Sails, check out: 10 | * http://sailsjs.org/#!/documentation/concepts/Internationalization 11 | * 12 | * For a complete list of i18n options, see: 13 | * https://github.com/mashpie/i18n-node#list-of-configuration-options 14 | * 15 | * 16 | */ 17 | 18 | module.exports.i18n = { 19 | 20 | /*************************************************************************** 21 | * * 22 | * Which locales are supported? * 23 | * * 24 | ***************************************************************************/ 25 | 26 | // locales: ['en', 'es', 'fr', 'de'], 27 | 28 | /**************************************************************************** 29 | * * 30 | * What is the default locale for the site? Note that this setting will be * 31 | * overridden for any request that sends an "Accept-Language" header (i.e. * 32 | * most browsers), but it's still useful if you need to localize the * 33 | * response for requests made by non-browser clients (e.g. cURL). * 34 | * * 35 | ****************************************************************************/ 36 | 37 | // defaultLocale: 'en', 38 | 39 | /**************************************************************************** 40 | * * 41 | * Automatically add new keys to locale (translation) files when they are * 42 | * encountered during a request? * 43 | * * 44 | ****************************************************************************/ 45 | 46 | // updateFiles: false, 47 | 48 | /**************************************************************************** 49 | * * 50 | * Path (relative to app root) of directory to store locale (translation) * 51 | * files in. * 52 | * * 53 | ****************************************************************************/ 54 | 55 | // localesDirectory: '/config/locales' 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /config/locales/_README.md: -------------------------------------------------------------------------------- 1 | # Internationalization / Localization Settings 2 | 3 | > Also see the official docs on internationalization/localization: 4 | > http://links.sailsjs.org/docs/config/locales 5 | 6 | ## Locales 7 | All locale files live under `config/locales`. Here is where you can add translations 8 | as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on request headers. 9 | 10 | Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): 11 | ```json 12 | { 13 | "Hello!": "Hola!", 14 | "Hello %s, how are you today?": "¿Hola %s, como estas?", 15 | } 16 | ``` 17 | ## Usage 18 | Locales can be accessed in controllers/policies through `res.i18n()`, or in views through the `__(key)` or `i18n(key)` functions. 19 | Remember that the keys are case sensitive and require exact key matches, e.g. 20 | 21 | ```ejs 22 |<%= i18n('That\'s right-- you can use either i18n() or __()') %>
25 | ``` 26 | 27 | ## Configuration 28 | Localization/internationalization config can be found in `config/i18n.js`, from where you can set your supported locales. 29 | -------------------------------------------------------------------------------- /config/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Willkommen", 3 | "A brand new app.": "Eine neue App." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Welcome", 3 | "A brand new app.": "A brand new app." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenido", 3 | "A brand new app.": "Una aplicación de la nueva marca." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenue", 3 | "A brand new app.": "Une toute nouvelle application." 4 | } 5 | -------------------------------------------------------------------------------- /config/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Log Configuration 3 | * (sails.config.log) 4 | * 5 | * Configure the log level for your app, as well as the transport 6 | * (Underneath the covers, Sails uses Winston for logging, which 7 | * allows for some pretty neat custom transports/adapters for log messages) 8 | * 9 | * For more information on the Sails logger, check out: 10 | * http://sailsjs.org/#!/documentation/concepts/Logging 11 | */ 12 | 13 | module.exports.log = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * Valid `level` configs: i.e. the minimum log level to capture with * 18 | * sails.log.*() * 19 | * * 20 | * The order of precedence for log levels from lowest to highest is: * 21 | * silly, verbose, info, debug, warn, error * 22 | * * 23 | * You may also set the level to "silent" to suppress all logs. * 24 | * * 25 | ***************************************************************************/ 26 | 27 | // level: 'info' 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /config/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default model configuration 3 | * (sails.config.models) 4 | * 5 | * Unless you override them, the following properties will be included 6 | * in each of your models. 7 | * 8 | * For more info on Sails models, see: 9 | * http://sailsjs.org/#!/documentation/concepts/ORM 10 | */ 11 | 12 | module.exports.models = { 13 | 14 | // Your app's default datastore. i.e. the name of one of your app's datastores (see `config/datastores.js`) 15 | // The former `connection` model setting is now `datastore`. This sets the datastore 16 | // that models will use, unless overridden directly in the model file in `api/models`. 17 | datastore: 'postgresql', 18 | 19 | /*************************************************************************** 20 | * * 21 | * How and whether Sails will attempt to automatically rebuild the * 22 | * tables/collections/etc. in your schema. * 23 | * * 24 | * See http://sailsjs.org/#!/documentation/concepts/ORM/model-settings.html * 25 | * * 26 | ***************************************************************************/ 27 | migrate: 'alter', 28 | 29 | schema: true, 30 | 31 | // These settings make the .update(), .create() and .createEach() 32 | // work like they did in 0.12, by returning records in the callback. 33 | // This is pretty ineffecient, so if you don't _always_ need this feature, you 34 | // should turn these off and instead chain `.meta({fetch: true})` onto the 35 | // individual calls where you _do_ need records returned. 36 | fetchRecordsOnUpdate: true, 37 | fetchRecordsOnCreate: true, 38 | fetchRecordsOnCreateEach: true, 39 | 40 | // Fetching records on destroy was experimental, but if you were using it, 41 | // uncomment the next line. 42 | // fetchRecordsOnDestroy: true, 43 | 44 | // Because you can't have the old `connection` setting at the same time as the new 45 | // `datastore` setting, we'll set it to `null` here. When you merge this file into your 46 | // existing `config/models.js` file, just remove any reference to `connection`. 47 | connection: null, 48 | 49 | // These attributes will be added to all of your models. When you create a new Sails 1.0 50 | // app with "sails new", a similar configuration will be generated for you. 51 | attributes: { 52 | // In Sails 1.0, the `autoCreatedAt` and `autoUpdatedAt` model settings 53 | // have been removed. Instead, you choose which attributes (if any) to use as 54 | // timestamps. By default, "sails new" will generate these two attributes as numbers, 55 | // giving you the most flexibility. But for compatibility with your existing project, 56 | // we'll define them as strings. 57 | createdAt: { type: 'string', autoCreatedAt: true, }, 58 | updatedAt: { type: 'string', autoUpdatedAt: true, }, 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your controllers. 6 | * You can apply one or more policies to a given controller, or protect 7 | * its actions individually. 8 | * 9 | * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed 10 | * below by its filename, minus the extension, (e.g. "authenticated") 11 | * 12 | * For more information on how policies work, see: 13 | * http://sailsjs.org/#!/documentation/concepts/Policies 14 | * 15 | * For more information on configuring policies, check out: 16 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.policies.html 17 | */ 18 | 19 | 20 | module.exports.policies = { 21 | 22 | /*************************************************************************** 23 | * * 24 | * Default policy for all controllers and actions (`true` allows public * 25 | * access) * 26 | * * 27 | ***************************************************************************/ 28 | 29 | '*': true, 30 | 31 | AssetController: { 32 | create: 'authToken', 33 | update: 'authToken', 34 | destroy: 'authToken', 35 | download: 'noCache' 36 | }, 37 | 38 | ChannelController: { 39 | create: 'authToken', 40 | update: 'authToken', 41 | destroy: 'authToken' 42 | }, 43 | 44 | FlavorController: { 45 | create: 'authToken', 46 | update: 'authToken', 47 | destroy: 'authToken' 48 | }, 49 | 50 | VersionController: { 51 | create: 'authToken', 52 | update: 'authToken', 53 | destroy: 'authToken', 54 | availability: 'authToken', 55 | redirect: 'noCache', 56 | general: 'noCache', 57 | windows: 'noCache', 58 | releaseNotes: 'noCache' 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Mappings 3 | * (sails.config.routes) 4 | * 5 | * Your routes map URLs to views and controllers. 6 | * 7 | * If Sails receives a URL that doesn't match any of the routes below, 8 | * it will check for matching files (images, scripts, stylesheets, etc.) 9 | * in your assets directory. e.g. `http://localhost:1337/images/foo.jpg` 10 | * might match an image file: `/assets/images/foo.jpg` 11 | * 12 | * Finally, if those don't match either, the default 404 handler is triggered. 13 | * See `api/responses/notFound.js` to adjust your app's 404 logic. 14 | * 15 | * Note: Sails doesn't ACTUALLY serve stuff from `assets`-- the default Gruntfile in Sails copies 16 | * flat files from `assets` to `.tmp/public`. This allows you to do things like compile LESS or 17 | * CoffeeScript for the front-end. 18 | * 19 | * For more information on configuring custom routes, check out: 20 | * http://sailsjs.org/#!/documentation/concepts/Routes/RouteTargetSyntax.html 21 | */ 22 | 23 | module.exports.routes = { 24 | 25 | '/': { view: 'homepage' }, 26 | '/home': { view: 'homepage' }, 27 | '/releases/:channel?': { view: 'homepage' }, 28 | '/admin': { view: 'homepage' }, 29 | '/auth/login': { view: 'homepage' }, 30 | '/auth/logout': { view: 'homepage' }, 31 | 32 | 'PUT /version/availability/:version/:timestamp': 'VersionController.availability', 33 | 34 | 'GET /download/latest/:platform?': 'AssetController.download', 35 | 'GET /download/channel/:channel/:platform?': 'AssetController.download', 36 | 'GET /download/:version/:platform?/:filename?': { 37 | controller: 'AssetController', 38 | action: 'download', 39 | // This is important since it allows matching with filenames. 40 | skipAssets: false 41 | }, 42 | 'GET /download/flavor/:flavor/latest/:platform?': 'AssetController.download', 43 | 'GET /download/flavor/:flavor/channel/:channel/:platform?': 'AssetController.download', 44 | 'GET /download/flavor/:flavor/:version/:platform?/:filename?': { 45 | controller: 'AssetController', 46 | action: 'download', 47 | // This is important since it allows matching with filenames. 48 | skipAssets: false 49 | }, 50 | 51 | 'GET /update': 'VersionController.redirect', 52 | 'GET /update/:platform/latest-mac.yml': 'VersionController.electronUpdaterMac', 53 | 'GET /update/:platform/:channel-mac.yml': 'VersionController.electronUpdaterMac', 54 | 'GET /update/:platform/latest.yml': 'VersionController.electronUpdaterWin', 55 | 'GET /update/:platform/:channel.yml': 'VersionController.electronUpdaterWin', 56 | 'GET /update/:platform/:version': 'VersionController.general', 57 | 'GET /update/:platform/:channel/latest.yml': 'VersionController.electronUpdaterWin', 58 | 'GET /update/:platform/:channel/latest-mac.yml': 'VersionController.electronUpdaterMac', 59 | 'GET /update/:platform/:version/RELEASES': 'VersionController.windows', 60 | 'GET /update/:platform/:version/:channel/RELEASES': 'VersionController.windows', 61 | 'GET /update/:platform/:version/:channel': 'VersionController.general', 62 | 63 | 'GET /update/flavor/:flavor/:platform/:version/:channel?': 'VersionController.general', 64 | 'GET /update/flavor/:flavor/:platform/:version/RELEASES': 'VersionController.windows', 65 | 'GET /update/flavor/:flavor/:platform/:version/:channel/RELEASES': 'VersionController.windows', 66 | 'GET /update/flavor/:flavor/:platform/latest.yml': 'VersionController.electronUpdaterWin', 67 | 'GET /update/flavor/:flavor/:platform/:channel.yml': 'VersionController.electronUpdaterWin', 68 | 'GET /update/flavor/:flavor/:platform/:channel/latest.yml': 'VersionController.electronUpdaterWin', 69 | 'GET /update/flavor/:flavor/:platform/latest-mac.yml': 'VersionController.electronUpdaterMac', 70 | 'GET /update/flavor/:flavor/:platform/:channel-mac.yml': 'VersionController.electronUpdaterMac', 71 | 'GET /update/flavor/:flavor/:platform/:channel/latest-mac.yml': 'VersionController.electronUpdaterMac', 72 | 73 | 'GET /notes/:version/:flavor?': 'VersionController.releaseNotes', 74 | 75 | 'GET /versions/sorted': 'VersionController.list', 76 | 77 | 'GET /channels/sorted': 'ChannelController.list' 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Configuration 3 | * (sails.config.session) 4 | * 5 | * Sails session integration leans heavily on the great work already done by 6 | * Express, but also unifies Socket.io with the Connect session store. It uses 7 | * Connect's cookie parser to normalize configuration differences between Express 8 | * and Socket.io and hooks into Sails' middleware interpreter to allow you to access 9 | * and auto-save to `req.session` with Socket.io the same way you would with Express. 10 | * 11 | * For more information on configuring the session, check out: 12 | * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.session.html 13 | */ 14 | 15 | module.exports.session = { 16 | // XXX: Enable this if you are using postgres as your database 17 | // If so, be sure to run the sql command detailed here: https://github.com/ravitej91/sails-pg-session 18 | 19 | // // uncomment if you use v2land-sails-pg-session 20 | // postgresql: { 21 | // adapter: 'v2land-sails-pg-session', 22 | // host: 'localhost', 23 | // user: 'electron_release_server_user', 24 | // password: 'MySecurePassword', 25 | // database: 'electron_release_server' 26 | // } 27 | 28 | // // uncomment if you use sails-pg-session 29 | // postgresql: { 30 | // adapter: 'sails-pg-session', 31 | // host: 'localhost', 32 | // user: 'electron_release_server_user', 33 | // password: 'MySecurePassword', 34 | // database: 'electron_release_server' 35 | // } 36 | }; 37 | -------------------------------------------------------------------------------- /config/views.js: -------------------------------------------------------------------------------- 1 | /** 2 | * View Engine Configuration 3 | * (sails.config.views) 4 | * 5 | * Server-sent views are a classic and effective way to get your app up 6 | * and running. Views are normally served from controllers. Below, you can 7 | * configure your templating language/framework of choice and configure 8 | * Sails' layout support. 9 | * 10 | * For more information on views and layouts, check out: 11 | * http://sailsjs.org/#!/documentation/concepts/Views 12 | */ 13 | 14 | module.exports.views = { 15 | 16 | /**************************************************************************** 17 | * * 18 | * View engine (aka template language) to use for your app's *server-side* * 19 | * views * 20 | * * 21 | * Sails+Express supports all view engines which implement TJ Holowaychuk's * 22 | * `consolidate.js`, including, but not limited to: * 23 | * * 24 | * ejs, pug, handlebars, mustache underscore, hogan, haml, haml-coffee, * 25 | * dust atpl, eco, ect, jazz, jqtpl, JUST, liquor, QEJS, swig, templayed, * 26 | * toffee, walrus, & whiskers * 27 | * * 28 | * For more options, check out the docs: * 29 | * https://github.com/balderdashy/sails-wiki/blob/0.9/config.views.md#engine * 30 | * * 31 | ****************************************************************************/ 32 | 33 | extension: 'pug', 34 | getRenderFn: function () { 35 | // Import `consolidate`. 36 | var consolidate = require('consolidate'); 37 | // Return the rendering function for Pug. 38 | return consolidate.pug; 39 | }, 40 | 41 | layout: false, 42 | 43 | /** 44 | * How many releases are retrieve from the API at a time 45 | */ 46 | pageSize: 50 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /database.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "defaultEnv": "local", 3 | 4 | "local": { 5 | "driver": "pg", 6 | "host": "DATABASE_HOST", 7 | "user": "DATABASE_USERNAME", 8 | "password": "DATABASE_PASSWORD", 9 | "database": "MAIN_DATABASE_NAME" 10 | }, 11 | 12 | "docker": { 13 | "driver": "pg", 14 | "host": {"ENV": "DB_HOST"}, 15 | "user": {"ENV": "DB_USERNAME"}, 16 | "password": {"ENV": "DB_PASSWORD"}, 17 | "database": {"ENV": "DB_NAME"} 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | build: . 5 | environment: 6 | WEBSITE_TITLE: 'Test Title' 7 | WEBSITE_HOME_CONTENT: 'Test Content' 8 | WEBSITE_NAV_LOGO: '' 9 | WEBSITE_APP_TITLE: 'Test App Title' 10 | APP_USERNAME: username 11 | APP_PASSWORD: password 12 | DB_HOST: db 13 | DB_PORT: 5432 14 | DB_USERNAME: releaseserver 15 | DB_NAME: releaseserver 16 | DB_PASSWORD: secret 17 | # DEKs should be 32 bytes long, and cryptographically random. 18 | # You can generate such a key by running the following: 19 | # require('crypto').randomBytes(32).toString('base64') 20 | # PLEASE ENSURE THAT YOU CHANGE THIS VALUE IN PRODUCTION 21 | DATA_ENCRYPTION_KEY: oIh0YgyxQbShuMjw4/laYcZnGKzvC3UniWFsqL0t4Zs= 22 | # Recommended: 63 random alpha-numeric characters 23 | # Generate using: https://www.grc.com/passwords.htm 24 | TOKEN_SECRET: change_me_in_production 25 | APP_URL: 'localhost:8080' 26 | ASSETS_PATH: '/usr/src/electron-release-server/releases' 27 | depends_on: 28 | - db 29 | ports: 30 | - '8080:80' 31 | entrypoint: ./scripts/wait.sh db:5432 -- npm start 32 | volumes: 33 | - ./releases:/usr/src/electron-release-server/releases 34 | db: 35 | image: postgres:11 36 | environment: 37 | POSTGRES_PASSWORD: secret 38 | POSTGRES_USER: releaseserver 39 | volumes: 40 | - postgres:/var/lib/postgresql/data 41 | 42 | volumes: 43 | postgres: 44 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Electron Release Server Documentation 2 | Please make sure that you use the documents that match your Electron Release Server version. 3 | 4 | ### FAQ 5 | There are questions that are asked quite often, [check this out before creating an issue](faq.md). 6 | 7 | ### Guides 8 | - [Deploy it!](deploy.md) 9 | - [Database setup](database.md) 10 | - [Admin authentication](authentication.md) 11 | - [Upload assets](assets.md) 12 | - [Customization](customization.md) 13 | - [Available URLs](urls.md) 14 | - [OS X Auto-Updater](update-osx.md) 15 | - [Windows Auto-Updater](update-windows.md) 16 | - [Docker](docker.md) 17 | - (Coming soon) [Use it as a node module](module.md) 18 | 19 | Using Electron Release Server for your application? [Add it to the list](using-it.md). 20 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | A metadata API is available to access more information about releases. 3 | 4 | There is also a more extensive RESTful API generated by [Sails](http://sailsjs.org) for managing releases. 5 | This private API is used by the Admin UI & is authenticated through LDAP by default, but you can customize this for your own use [without too much difficulty](deploy.md)! 6 | 7 | > Note that since the addition of flavors, version id is formatted as `name_flavor`. 8 | 9 | #### List versions: 10 | ``` 11 | GET http://download.myapp.com/versions/sorted 12 | ``` 13 | 14 | #### Get details about specific version: 15 | ``` 16 | GET http://download.myapp.com/api/version/1.1.0_default 17 | ``` 18 | 19 | #### Set availability date of specific version (Unix timestamp in milliseconds): 20 | ``` 21 | PUT http://download.myapp.com/version/availability/1.1.0_default/1574755200000 22 | ``` 23 | 24 | #### List channels: 25 | ``` 26 | GET http://download.myapp.com/channels/sorted 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/assets.md: -------------------------------------------------------------------------------- 1 | # Adding Assets 2 | Adding assets/versions couldn't be easier. 3 | 4 | Check the video below for a brief video of how to add a version and assets. 5 | 6 | [](https://youtu.be/lvT7rfB01iA) 7 | 8 | Once added, assets and versions will be instantly available on their channels. This is great for quickly distributing new versions to your users when paired with Electron's built-in auto-updater. 9 | 10 | ## Files to upload 11 | The release server will process and serve files for a given version based on two heuristics. 12 | 13 | #### Platform 14 | This is explicitly defined when uploading the asset. 15 | 16 | #### File extension 17 | This will tell the service whether the file is meant for updates or initial installation. 18 | 19 | ### OS X 20 | #### Initial installation 21 | Accepted file extensions: 22 | - `.dmg` 23 | 24 | #### Serving updates 25 | Accepted file extensions: 26 | - `.zip` 27 | 28 | ### Windows 29 | #### Initial installation 30 | Accepted file extensions: 31 | - `.exe` 32 | 33 | #### Serving updates 34 | Accepted file extensions: 35 | - `.nupkg` 36 | - `.blockmap` (NSIS differential updates) 37 | 38 | **Important**: only `-full.nupkg` files are currently supported. If you're confused, just upload the one that [electron-builder](https://github.com/electron-userland/electron-builder) made for you. 39 | 40 | > Note that you do not have to upload the `RELEASES` file because one will be generated upon request. 41 | 42 | ### Linux 43 | #### Initial installation 44 | Accepted file extensions: 45 | - `.deb` 46 | 47 | #### Serving updates 48 | The Electron auto-updater does not support Linux and neither does this. 49 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication Setup 2 | The admin interface authenticates with the back end to check whether a user has admin permissions. 3 | 4 | Electron release server supports a couple authentication methods out of the box. 5 | You must specify configuration details for at least one of them in the `config/local.js` in the `auth` sub object. 6 | 7 | If you do not wish to use an authentication method, ensure that it's config sub object is not present in the aforementioned config file. 8 | 9 | ## Supported methods 10 | ### Static 11 | Authenticates credentials against a single username/password pair. 12 | 13 | If enabled this will take precedence over other enabled methods. 14 | #### Config 15 | ```js 16 | static: { 17 | username: 'STATIC_USERNAME', 18 | password: 'STATIC_PASSWORD' 19 | } 20 | ``` 21 | 22 | ### LDAP 23 | Authenticates credentials against an LDAP service, optionally filtering results (ex. only a given group has access). 24 | 25 | #### Config 26 | ```js 27 | ldap: { 28 | usernameField: 'USERNAME_FIELD', // Key at which the username is stored 29 | server: { 30 | url: 'ldap://LDAP_SERVER_FQDN:389', 31 | bindDn: 'INSERT_LDAP_SERVICE_ACCOUNT_USERNAME_HERE', 32 | bindCredentials: 'INSERT_PASSWORD_HERE', 33 | searchBase: 'USER_SEARCH_SPACE', // ex: ou=Our Users,dc=companyname,dc=com 34 | searchFilter: '(USERNAME_FIELD={{username}})' 35 | } 36 | } 37 | ``` 38 | 39 | ### Custom 40 | There is a good chance that you will want to modify the authentication method used to match your needs. 41 | 42 | Please consider making said changes in a fork and opening a PR so that everyone can benefit from your work. 43 | 44 | You can do so by modifying the file found here: `api/services/AuthService.js`. 45 | -------------------------------------------------------------------------------- /docs/customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | Although this project is infinitely customizable through the process of forking the repo and making code changes, users that are satisfied with minimal branding can achieve this through environment variables. 3 | 4 | The following environment variables can be set to customize the website: 5 | - `WEBSITE_APP_TITLE`: The app name to use throughout the website. 6 | - `WEBSITE_TITLE`: The title text to show on the home page and to use as the page title. 7 | - `WEBSITE_HOME_CONTENT`: Content to display below the title on the home page. 8 | - `WEBSITE_NAV_LOGO`: A url to a logo image that will be shown in place of the default Electron logo. 9 | 10 | If you are deploying the app through `docker-compose`, then you can simply adjust these parameters in `docker-compose.yml`. 11 | -------------------------------------------------------------------------------- /docs/database.md: -------------------------------------------------------------------------------- 1 | # Database Setup 2 | The following is a recommended database setup process. 3 | 4 | ## Prerequisites 5 | Before you continue, you will need to have an instance on the PostgresSQL database running on a host. 6 | 7 | You will additionally need to have the following details handy (see instruction below): 8 | - Host name 9 | - The username & password for an account with privileges to create a user, create a database & 10 | assign a user to the database. 11 | 12 | If you need help installing the database, the following link contains 13 | detailed [installaing guides](https://wiki.postgresql.org/wiki/Detailed_installation_guides). 14 | 15 | ## PostgreSQL 16 | Using the details listed in the prerequisites, connect to the PostgreSQL database using 17 | psql ([PostgreSQL interactive terminal](http://www.postgresql.org/docs/9.2/static/app-psql.html)). 18 | You need to connect as admin user _postgres_ 19 | ~~~ 20 | C:\Program Files\PostgreSQL\9.5\bin>psql.exe --username=postgres 21 | ~~~ 22 | 23 | 24 | You now need to create dedicated _postgres_ role using PostgreSQL terminal syntax 25 | ```sql 26 | CREATE ROLE electron_release_server_user ENCRYPTED PASSWORD '