├── .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 | [![GitHub stars](https://img.shields.io/github/stars/ArekSredzki/electron-release-server.svg)](https://github.com/ArekSredzki/electron-release-server/stargazers) 3 | [![GitHub forks](https://img.shields.io/github/forks/ArekSredzki/electron-release-server.svg)](https://github.com/ArekSredzki/electron-release-server/network) 4 | [![Join the chat at https://gitter.im/ArekSredzki/electron-release-server](https://badges.gitter.im/ArekSredzki/electron-release-server.svg)](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 | [![Electron Release Server Demo](https://j.gifs.com/wpyY1X.gif)](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 | 3 | 4 | 5 | 6 | 7 | 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 | *
24 | * 25 | *
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 |

<%= __('Welcome to PencilPals!') %>

23 |

<%= i18n('Hello %s, how are you today?', 'Pencil Maven') %>

24 |

<%= 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 | [![Electron Release Server Demo](https://j.gifs.com/wpyY1X.gif)](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 '' LOGIN; 27 | ``` 28 | 29 | Example: 30 | ~~~sql 31 | CREATE ROLE electron_release_server_user ENCRYPTED PASSWORD 'MySecurePassword' LOGIN; 32 | ~~~ 33 | > Hint: if you need a password, use this https://www.grc.com/passwords.htm 34 | > 35 | > (63 random alpha-numeric characters) 36 | 37 | 38 | Create databases for created role (_electron_release_server_user_) 39 | ```sql 40 | CREATE DATABASE electron_release_server OWNER "electron_release_server_user"; 41 | CREATE DATABASE electron_release_server_sessions OWNER "electron_release_server_user"; 42 | ``` 43 | After completing this section, you should now have 44 | 1. host - hostname of your postgres database server 45 | 2. user - username to be used by the application (electron_release_server_user) 46 | 3. password - password to be used by the application 47 | 4. server database - database name for the server (electron_release_server) 48 | 5. server database - database name for the server (electron_release_server_sessions) 49 | > Hint: You now need to ensure that these settings are reflected in the `config/local.js` file. 50 | 51 | ``` 52 | datastores: { 53 | postgresql: { 54 | adapter: 'sails-postgresql', 55 | host: 'localhost', 56 | user: 'electron_release_server_user', 57 | password: 'MySecurePassword', 58 | database: 'electron_release_server' 59 | } 60 | }, 61 | 62 | session: { 63 | // Recommended: 63 random alpha-numeric characters 64 | // Generate using: https://www.grc.com/passwords.htm 65 | secret: 'EB9F0CA4414893F7B72DDF0F8507D88042DB4DBF8BD9D0A5279ADB54158EB2F0', 66 | database: 'electron_release_server_sessions', 67 | host: 'localhost', 68 | user: 'electron_release_server_user', 69 | password: 'MySecurePassword', 70 | port: 5432 71 | } 72 | ``` 73 | 74 | ### Session adapter requirements 75 | For the session adapter you can use one of the following adapters: 76 | - v2land-sails-pg-session 77 | - sails-pg-session 78 | 79 | For **_v2land-sails-pg-session_** use following sample, navigate to your GIT folder with repos and run 80 | ```bash 81 | mkdir sails_pg_session_v2 82 | cd sails_pg_session_v2 83 | git clone https://github.com/v2land/sails-pg-session.git . 84 | npm update 85 | psql electron_release_server_sessions < ./sql/sails-pg-session-support.sql postgres 86 | ``` 87 | 88 | If you use **_sails-pg-session_** then follow next sample, navigate to your GIT folder with repos and run 89 | ```bash 90 | mkdir sails_pg_session 91 | cd sails_pg_session 92 | git clone https://github.com/ravitej91/sails-pg-session.git . 93 | npm update 94 | psql electron_release_server_sessions < ./sql/sails-pg-session-support.sql postgres 95 | ``` 96 | 97 | In order to use Database you need to update `config/session.js`. For you comfort snippets were added in existing file. 98 | Uncomment snippet to enable adapter of your choice (either _v2land-sails-pg-session_ or _sails-pg-session_) 99 | 100 | > Hint: Use the same **process** as mentioned above to generate the session & 101 | >JWT secret keys stored in `config/local.js` 102 | > 103 | 104 | Originally described [here](https://github.com/ravitej91/sails-pg-session) 105 | 106 | ## Issues 107 | Please refer to Frequently Asked Questions section of documentation in case if you still see some issue 108 | 109 | ## Migration 110 | If you have a pre-existing database and want to update to a new version of Electron Release Server that has database 111 | changes, then you may be able to use migration scripts stored in [migrations](../migrations/). 112 | 113 | The migration framework being used is [db-migrate](https://www.npmjs.com/package/db-migrate) and in order for it to 114 | work you will need to copy `database.json.template` to `database.json` and then change the applicable settings to 115 | match your database setup. 116 | 117 | To use these migration scripts you can run `npm run migrate up` to apply the changes to your database. You also have 118 | the option to undo the changes by running `npm run migrate down`. 119 | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | Electron Release Server can be easily be deployed to your own server. 4 | 5 | ## General Configuration: 6 | 7 | Install dependencies using: 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | **Action Step:** You need to configure database ([database setup guide](database.md)) and must create a `config/local.js` file, which contains the configuration options required to run the server. 14 | 15 | To assist this process, you can copy `config/local.template` and edit it using: 16 | ```bash 17 | cp config/local.template config/local.js 18 | vim config/local.js 19 | ``` 20 | 21 | Then start the application using: 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | Browse to `http://localhost:1337/` 28 | 29 | ## Using Nginx 30 | 31 | If you want to use nginx as web-server: 32 | 33 | ```nginx 34 | server { 35 | listen 80; 36 | server_name download.yourdomain.com; 37 | 38 | location / { 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | proxy_set_header Host $http_host; 42 | proxy_set_header X-Nginx-Proxy true; 43 | 44 | proxy_pass http://127.0.0.1:1337/; 45 | proxy_redirect off; 46 | proxy_http_version 1.1; 47 | proxy_set_header Upgrade $http_upgrade; 48 | proxy_set_header Connection "upgrade"; 49 | } 50 | 51 | error_page 500 502 503 504 /50x.html; 52 | location = /50x.html { 53 | root /usr/share/nginx/html; 54 | } 55 | } 56 | ``` 57 | 58 | Browse to `http://download.yourdomain.com/` 59 | 60 | ## Database setup 61 | See the [database setup guide](database.md). 62 | 63 | ## Authentication 64 | See the [authentication guide](authentication.md). 65 | 66 | ## Deployment 67 | See the [Sails deployment documentation](http://sailsjs.org/documentation/concepts/deployment). 68 | 69 | To start the server in deployment mode use: 70 | ``` 71 | npm start --prod 72 | ``` 73 | 74 | > Note: In production you should use a process manager such as [pm2](http://pm2.keymetrics.io/) 75 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | Electron Release Server has a `Dockerfile` and a `docker-compose.yml`file. 4 | So, you can use [docker](https://www.docker.com/) and [docker-compose](https://github.com/docker/compose). 5 | 6 | ## Requirements 7 | 8 | Install [docker](https://www.docker.com/) and [docker-compose](https://github.com/docker/compose). 9 | 10 | ## Localserver 11 | 12 | ```bash 13 | docker-compose up 14 | # open localhost:8080 in browser 15 | ``` 16 | 17 | If you use [docker-machine](https://github.com/docker/machine) you should change 18 | `APP_URL` at `docker-compose.yml` to address to your docker-machine. 19 | 20 | ## Configurations 21 | 22 | To run the single container provide the next environment variables: 23 | 24 | - `APP_USERNAME`, `APP_PASSWORD` – static username and password for authentication ref. 25 | - `DB_HOST` – hostname of postgres 26 | - `DB_PORT` – port of postgres 27 | - `DB_USERNAME`, `DB_PASSWORD` – credentials to access postgres 28 | - `DB_NAME` – Database name 29 | - `DATA_ENCRYPTION_KEY` - DEKs should be 32 bytes long, and cryptographically random. 30 | You can generate such a key by running the following: 31 | ``` 32 | require('crypto').randomBytes(32).toString('base64') 33 | ``` 34 | - `TOKEN_SECRET` – Recommended: 63 random alpha-numeric characters 35 | - `APP_URL` - base url for the app - [ref](http://sailsjs.org/documentation/reference/application/sails-get-base-url) 36 | 37 | To use `production.js` set `NODE_ENV` to `"production"` – so you should not set the environment variables: 38 | `APP_USERNAME`, `APP_PASSWORD`, `DB_HOST`, `DB_PORT`, 39 | `DB_USERNAME`, `DB_PASSWORD`, `DB_NAME`, `TOKEN_SECRET`. 40 | 41 | **Warning**: You can insert the `APP_PASSWORD`, `DB_PASSWORD`, `TOKEN_SECRET`, and `DATA_ENCRYPTION_KEY` directly into 42 | the `docker-compose.yml`, but this is not advised since it makes it easy to accidentally publish your secretms. 43 | The production secrets must not be committed publicly! 44 | 45 | ## How to run 46 | 47 | For your first run, you should start with development settings (which are the default) since this will perform database initialization/migration. 48 | 49 | For all subsequent executions, you should run in production mode by setting `NODE_ENV` to `"production"`. 50 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Electron Release Server FAQ 2 | 3 | ### What files should I upload? 4 | 5 | Electron Release Server uses explicit file compatibility naming in order to avoid unexpected issues, there is no strict policy on file naming. 6 | 7 | - Windows: `.exe`, `.nupkg` etc 8 | - Linux: `.deb`, `.tar.gz`, etc 9 | - OS X: `.dmg`, etc 10 | 11 | 32 bit releases are made available to all clients, but 64 bit files are served to compatible clients if available. 12 | 13 | ### How should I name my releases? 14 | 15 | Electron Release Server requires applications to follow [SemVer](http://semver.org). And even if you're not using Electron Release Server, you should follow it! 16 | 17 | ### I'm seeing HTTP errors when the Electron autoUpdater queries for the `RELEASES` file. How should I fix it? 18 | 19 | Ensure that you are not including `/RELEASES` in the feed URL that is passed to `setFeedURL()`. 20 | 21 | ### Why do I see `password authentication failed`? 22 | 23 | When you run your server (usually on Windows machine) you may see following error message: 24 | ~~~ 25 | error: password authentication failed for user "electron_release_server_user" 26 | ~~~ 27 | 28 | Solution could be to update server configuration file: 29 | Windows: 30 | ~~~ 31 | C:\Program Files\PostgreSQL\12\data\pg_hba.conf 32 | ~~~ 33 | Linux: 34 | ~~~ 35 | /var/lib/pgsql/data/pg_hba.conf 36 | ~~~ 37 | 38 | to make all METHODs trusted 39 | ~~~ 40 | host all all 127.0.0.1/32 trust 41 | host all all ::1/128 trust 42 | host replication all 127.0.0.1/32 trust 43 | host replication all ::1/128 trust 44 | ~~~ 45 | _Note: do not forget to open services.msc and restart the server_ 46 | 47 | 48 | ### Error: Server failed to start, port 80 in use 49 | IF you see following error message one of the causes as listed in error message might be an already used port 50 | ~~~ 51 | error: Server failed to start. 52 | error: (received error: EACCES) 53 | error: 54 | error: Troubleshooting tips: 55 | error: 56 | error: -> Do you have a slow Grunt task, or lots of assets? 57 | error: 58 | error: -> Do you have permission to use port 80 on this system? 59 | error: 60 | error: -> Is something else already running on port 80 ? 61 | error: 62 | error: -> Are you deploying on a platform that requires an explicit hostname, like OpenShift? 63 | error: (Try setting the `explicitHost` config to the hostname where the server will be accessible.) 64 | error: (e.g. `mydomain.com` or `183.24.244.42`) 65 | ~~~ 66 | 67 | You can specify an environment variable PORT for the session (or permanent one) 68 | 69 | Windows PowerShell: 70 | ~~~ 71 | $env:PORT=1337 72 | ~~~ 73 | 74 | CMD: 75 | ~~~ 76 | set PORT=1337 77 | ~~~ 78 | 79 | Linux Bash: 80 | ~~~ 81 | export PORT=1337 82 | ~~~ 83 | 84 | See documentation for your console for appropriate syntax 85 | 86 | 87 | ### Error on startup on Linux 88 | When you start your application on Linux environment you may see following error message: 89 | ~~~ 90 | 91 | error: Grunt :: /home/git/release_server/node_modules/grunt-legacy-util/index.js:26 92 | var _ = util._ = require('lodash').runInContext(); 93 | ^ 94 | 95 | TypeError: require(...).runInContext is not a function 96 | at Object. (/home/git/release_server/node_modules/grunt-legacy-util/index.js:26:36) 97 | at Module._compile (internal/modules/cjs/loader.js:1138:30) 98 | at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10) 99 | at Module.load (internal/modules/cjs/loader.js:986:32) 100 | at Function.Module._load (internal/modules/cjs/loader.js:879:14) 101 | at Module.require (internal/modules/cjs/loader.js:1026:19) 102 | at require (internal/modules/cjs/helpers.js:72:18) 103 | at Object. (/home/git/release_server/node_modules/grunt/lib/grunt.js:35:12) 104 | at Module._compile (internal/modules/cjs/loader.js:1138:30) 105 | at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10) 106 | ~~~ 107 | 108 | To resolve the problem modify _./node_modules/grunt-legacy-util/index.js_ line 26 to: 109 | ~~~ 110 | var _ = util._ = require('lodash'); 111 | ~~~ 112 | -------------------------------------------------------------------------------- /docs/update-osx.md: -------------------------------------------------------------------------------- 1 | # Auto-updater on OS X 2 | Electron Release Server provides a backend for the [Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac) auto-updater. Squirrel.Mac is integrated by default in [Electron applications](https://github.com/atom/electron). 3 | 4 | ### Endpoint 5 | 6 | The endpoints for **Squirrel.Mac** are: 7 | - `https://download.myapp.com/update/:platform/:version[/:channel]` 8 | - `https://download.myapp.com/update/flavor/:flavor/:platform/:version[/:channel]`. 9 | 10 | Note that `version` is the currently installed version. 11 | 12 | The server will accept the platform as `osx`, `darwin`,`darwin_64`,`macos`, and `mac`. 13 | 14 | Since the server supports multiple release channels, you can specify the channel when requesting updates. Examples of supported channels are `stable`, `beta`, `alpha`. Each channel includes those above it; `beta` will include `stable` updates. 15 | 16 | This url requires different parameters to return a correct version: `version` and `platform`. 17 | 18 | If the flavor is not specified, then `default` will be used. 19 | 20 | ### Electron Example 21 | 22 | For example with Electron's [`autoUpdater`](https://github.com/electron/electron/blob/master/docs/api/auto-updater.md) module: 23 | 24 | ```js 25 | var app = require('app'); 26 | var os = require('os'); 27 | var autoUpdater = require('electron').autoUpdater; 28 | 29 | var platform = os.platform() + '_' + os.arch(); // usually returns darwin_64 30 | var version = app.getVersion(); 31 | var channel = 'stable'; 32 | 33 | autoUpdater.setFeedURL('http://download.myapp.com/update/' + platform + '/' + version + '/' + channel); 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/update-windows.md: -------------------------------------------------------------------------------- 1 | # Auto-updater on Windows 2 | Electron Release Server provides a backend for the [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows) auto-updater. 3 | 4 | Refer to the [Squirrel.Windows documentation](https://github.com/Squirrel/Squirrel.Windows/tree/master/docs) on how to setup your application. 5 | 6 | Electron Release Server will serve NuGet packages on the following endpoints: 7 | ``` 8 | http://download.myapp.com/update/win32/:version/RELEASES 9 | http://download.myapp.com/update/win64/:version/RELEASES 10 | http://download.myapp.com/update/win32/:version/:channel/RELEASES 11 | http://download.myapp.com/update/win64/:version/:channel/RELEASES 12 | http://download.myapp.com/update/flavor/:flavor/win32/:version/RELEASES 13 | http://download.myapp.com/update/flavor/:flavor/win64/:version/RELEASES 14 | http://download.myapp.com/update/flavor/:flavor/win32/:version/:channel/RELEASES 15 | http://download.myapp.com/update/flavor/:flavor/win64/:version/:channel/RELEASES 16 | ``` 17 | If the channel is not specified, then `stable` will be used. If the flavor is not specified, then `default` will be used. If `win64` is used but only a `win32` asset is available, it will be used. 18 | 19 | > Note: If desired, you can use `windows_32` instead of `win32` and `windows_64` instead of `win64`. 20 | 21 | You'll just need to configure your `Update.exe` or `Squirrel.Windows` to use `http://download.myapp.com/update/win32/:version/:channel` as a feed URL **without query parameters.** 22 | 23 | You'll just need to upload as release assets: `-full.nupkg` (files generated by `electron-builder` or `Squirrel.Windows` releaser). 24 | 25 | The RELEASES file will be generated for you :) 26 | 27 | ## NSIS differential updates 28 | Electron Release Server can also be used to serve NSIS differential updates for electron-builder. 29 | 30 | You should define the following endpoint as url: `https://my.update.server/update/${os}${arch}/`. Electron Release Server will auto-generate the `.yml` files for the corresponding channel (`latest.yml`, `beta.yml`, `alpha.yml`). Electron-builder will fetch the `.yml` for the correct channel, check if there is a newer version and fetch the `.blockmap` file of the new release. Based on the `.blockmap` file, it will download parts of the `.exe` file. The file name of the `.blockmap` should be the same as the `.exe` file but with the extra `.blockmap` extension. 31 | `useMultipleRangeRequest` is not supported by Electron Release Server, so it should be disabled. 32 | 33 | To get differential updates for Windows NSIS: 34 | 1. Point electron-builder to `https://my.update.server/update/${os}${arch}/` 35 | 2. Set [`"useMultipleRangeRequest": false`](https://www.electron.build/configuration/publish#genericserveroptions) 36 | 3. Upload the `.exe` and `.blockmap` files for the Windows (32-bit or 64-bit) build (and keep the `.blockmap` files available for all releases. Do not delete it!) 37 | 4. Electron-builder will fetch the `.yml` files (`https://my.update.server/update/win/latest.yml` or `https://my.update.server/update/win/beta.yml` or `https://my.update.server/update/win/alpha.yml`) and download the updates based on the `.blockmap`. 38 | 39 | Keep in mind that the counter for the `.exe` file will be incorrect because there will be multiple requests to the `.exe` file. However, the `.blockmap` counter should be accurate because this one is downloaded onces per update. 40 | -------------------------------------------------------------------------------- /docs/urls.md: -------------------------------------------------------------------------------- 1 | # Available URLs 2 | ## Download endpoints 3 | Electron Release Server provides a variety of urls to access release assets. 4 | 5 | #### Latest version for detected platform: 6 | - `http://download.myapp.com/download/latest` 7 | - `http://download.myapp.com/download/flavor/default/latest` 8 | #### Latest version for specific platform: 9 | - `http://download.myapp.com/download/latest/osx` 10 | - `http://download.myapp.com/download/flavor/default/latest/osx` 11 | #### Specific version for detected platform: 12 | - `http://download.myapp.com/download/1.1.0` 13 | - `http://download.myapp.com/download/flavor/default/1.1.0` 14 | #### Specific version for a specific platform: 15 | - `http://download.myapp.com/download/1.2.0/osx` 16 | - `http://download.myapp.com/download/flavor/default/1.2.0/osx` 17 | #### Specific file for a version for a specific platform: 18 | > Note that only the file extension is used. 19 | 20 | - `http://download.myapp.com/download/1.2.0/windows_64/MyApp-0.1.1-full.nupkg` 21 | - `http://download.myapp.com/download/flavor/default/1.2.0/windows_64/MyApp-0.1.1-full.nupkg` 22 | #### Specific channel: 23 | - `http://download.myapp.com/download/channel/beta` 24 | - `http://download.myapp.com/download/flavor/default/channel/beta` 25 | #### Specific channel for specific platform: 26 | - `http://download.myapp.com/download/channel/beta/osx` 27 | - `http://download.myapp.com/download/flavor/default/channel/beta/osx` 28 | 29 | ### Windows 30 | - `http://download.myapp.com/download/windows_32` 31 | - `http://download.myapp.com/download/windows_64` 32 | - `http://download.myapp.com/download/flavor/default/windows_32` 33 | - `http://download.myapp.com/download/flavor/default/windows_64` 34 | 35 | ### Linux 36 | - `http://download.myapp.com/download/linux_32` 37 | - `http://download.myapp.com/download/linux_64` 38 | - `http://download.myapp.com/download/flavor/default/linux_32` 39 | - `http://download.myapp.com/download/flavor/default/linux_64` 40 | 41 | ## Update endpoints 42 | These are detailed separately for [OSX](update-osx.md) and [Windows](update-windows.md). 43 | 44 | When an update is not available, the update endpoints will return a 204 response. This happens when the version you are requesting is newer than or equal to the last available version on the server, but also when the appropriate file type is not present for Squirrel to be able to update your application (`.zip` for Squirrel.Mac, `.nupkg` for Squirrel.Windows). 45 | 46 | ## Notes endpoint 47 | `http://download.myapp.com/notes/:version/:flavor?` 48 | 49 | ## Data endpoints 50 | These are detailed separately [here](api.md). 51 | 52 | ## About using HTTPS 53 | If you are using HTTPS on your server be sure to configure the base URL (`appUrl`) in `config/local.js` to use it as well since by default the download URLs will come from HTTP even if the update URL has been called from HTTPS. 54 | -------------------------------------------------------------------------------- /docs/using-it.md: -------------------------------------------------------------------------------- 1 | # Who is using it? 2 | 3 | Using Electron Release Server for your application? Create a Pull-Request! 4 | 5 | - Tesla Motors (internal use) 6 | - cloudtag.io sharing files. easier. 7 | -------------------------------------------------------------------------------- /migrations/20190930000000-asset-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { driver } = require('db-migrate').getInstance().config.getCurrent().settings; 4 | 5 | const sql = { 6 | 7 | // PostgreSQL 8 | pg: { 9 | up: ( 10 | 11 | // Add `id` column (if doesn't exist) to `asset` table 12 | 'ALTER TABLE asset ADD COLUMN IF NOT EXISTS id TEXT;' + 13 | 14 | // Populate `id` column in `asset` table with default data if empty 15 | 'UPDATE asset SET id = CONCAT(version, \'_\', platform, \'_\', REPLACE(filetype, \'.\', \'\')) WHERE id IS NULL;' + 16 | 17 | // Drop primary key constraint (if exists) from `asset` table 18 | 'ALTER TABLE asset DROP CONSTRAINT IF EXISTS asset_pkey;' + 19 | 20 | // Add primary key constraint on `id` column in `asset` table 21 | 'ALTER TABLE asset ADD PRIMARY KEY (id);' 22 | 23 | ), 24 | down: ( 25 | 26 | // Drop `id` column (if exists) from `asset` table 27 | 'ALTER TABLE asset DROP COLUMN IF EXISTS id;' + 28 | 29 | // Drop primary key constraint (if exists) from `asset` table 30 | 'ALTER TABLE asset DROP CONSTRAINT IF EXISTS asset_pkey;' + 31 | 32 | // Add primary key constraint on `name` column in `asset` table 33 | 'ALTER TABLE asset ADD PRIMARY KEY (name);' 34 | 35 | ) 36 | } 37 | 38 | }; 39 | 40 | const { up } = driver && sql[driver]; 41 | const { down } = driver && sql[driver]; 42 | 43 | exports.up = db => up ? db.runSql(up) : null; 44 | exports.down = db => down ? db.runSql(down) : null; 45 | -------------------------------------------------------------------------------- /migrations/20190930000000-availability-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { driver } = require('db-migrate').getInstance().config.getCurrent().settings; 4 | 5 | const sql = { 6 | 7 | // PostgreSQL 8 | pg: { 9 | up: ( 10 | 11 | // Add `availability` column (if doesn't exist) to `version` table 12 | 'ALTER TABLE version ADD COLUMN IF NOT EXISTS availability TIMESTAMPTZ;' 13 | 14 | ), 15 | down: ( 16 | 17 | // Drop `availability` column (if exists) from `version` table 18 | 'ALTER TABLE version DROP COLUMN IF EXISTS availability;' 19 | 20 | ) 21 | } 22 | 23 | }; 24 | 25 | const { up } = driver && sql[driver]; 26 | const { down } = driver && sql[driver]; 27 | 28 | exports.up = db => up ? db.runSql(up) : null; 29 | exports.down = db => down ? db.runSql(down) : null; 30 | -------------------------------------------------------------------------------- /migrations/20190930000000-flavor-migration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { driver } = require('db-migrate').getInstance().config.getCurrent().settings; 4 | 5 | const sql = { 6 | 7 | // PostgreSQL 8 | pg: { 9 | up: ( 10 | 11 | // Create `flavor` table (if doesn't exist) 12 | 'CREATE TABLE IF NOT EXISTS flavor (name TEXT PRIMARY KEY, "createdAt" TIMESTAMPTZ, "updatedAt" TIMESTAMPTZ);' + 13 | 14 | // Add `id` column (if doesn't exist) to `version` table 15 | 'ALTER TABLE version ADD COLUMN IF NOT EXISTS id TEXT;' + 16 | 17 | // Add `flavor` column (if doesn't exist) to `version` table 18 | 'ALTER TABLE version ADD COLUMN IF NOT EXISTS flavor TEXT;' + 19 | 20 | // Drop primary key constraint (if exists) from `version` table 21 | 'ALTER TABLE version DROP CONSTRAINT IF EXISTS version_pkey;' + 22 | 23 | // Populate `id` column in `version` table with default data if empty 24 | 'UPDATE version SET id = CONCAT(name, \'_\', COALESCE(flavor, \'default\')) WHERE id IS NULL;' + 25 | 26 | // Add primary key constraint on `id` column in `version` table 27 | 'ALTER TABLE version ADD PRIMARY KEY (id);' 28 | 29 | ), 30 | down: ( 31 | 32 | // Drop `flavor` table (if exists) 33 | 'DROP TABLE IF EXISTS flavor;' + 34 | 35 | // Drop `id` column (if exists) from `version` table 36 | 'ALTER TABLE version DROP COLUMN IF EXISTS id;' + 37 | 38 | // Drop `flavor` column (if exists) from `version` table 39 | 'ALTER TABLE version DROP COLUMN IF EXISTS flavor;' + 40 | 41 | // Drop primary key constraint (if exists) from `version` table 42 | 'ALTER TABLE version DROP CONSTRAINT IF EXISTS version_pkey;' + 43 | 44 | // Add primary key constraint on `name` column in `version` table 45 | 'ALTER TABLE version ADD PRIMARY KEY (name);' + 46 | 47 | // Depopulate default data from `version` column in `asset` table 48 | 'UPDATE asset SET version = REPLACE(version, \'_default\', \'\') WHERE version LIKE \'%_default\';' 49 | 50 | ) 51 | } 52 | 53 | }; 54 | 55 | const { up } = driver && sql[driver]; 56 | const { down } = driver && sql[driver]; 57 | 58 | exports.up = db => up ? db.runSql(up) : null; 59 | exports.down = db => down ? db.runSql(down) : null; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-release-server", 3 | "private": true, 4 | "version": "2.1.3", 5 | "description": "A version server for hosting and serving the your electron desktop app releases.", 6 | "dependencies": { 7 | "@sailshq/upgrade": "^1.0.9", 8 | "async": "^3.2.4", 9 | "bluebird": "^3.7.2", 10 | "bower": "^1.8.14", 11 | "compare-versions": "^4.1.4", 12 | "consolidate": "^0.16.0", 13 | "db-migrate": "^0.11.13", 14 | "db-migrate-pg": "^1.2.2", 15 | "express-useragent": "^1.0.15", 16 | "fs-extra": "^11.1.0", 17 | "grunt": "^1.5.3", 18 | "grunt-contrib-coffee": "^2.1.0", 19 | "grunt-contrib-jst": "^2.0.0", 20 | "grunt-contrib-pug": "^3.0.0", 21 | "grunt-contrib-uglify": "^5.2.2", 22 | "grunt-sass": "^3.1.0", 23 | "grunt-wiredep": "^3.0.1", 24 | "include-all": "~4.0.3", 25 | "jsonwebtoken": "^9.0.0", 26 | "ldapauth-fork": "^5.0.5", 27 | "lodash": "^4.17.21", 28 | "mime": "^3.0.0", 29 | "node-sass": "^8.0.0", 30 | "passport": "^0.6.0", 31 | "passport-ldapauth": "^3.0.1", 32 | "pug": "^3.0.2", 33 | "rc": "~1.2.8", 34 | "sails": "^1.5.3", 35 | "sails-disk": "^2.1.2", 36 | "sails-hook-grunt": "^5.0.0", 37 | "sails-hook-orm": "^4.0.2", 38 | "sails-hook-sockets": "^2.0.3", 39 | "sails-pg-session": "^1.0.1", 40 | "sails-postgresql": "^5.0.1", 41 | "semver": "^7.3.8", 42 | "skipper-disk": "^0.5.12", 43 | "strip-bom": "^4.0.0", 44 | "url": "^0.11.0", 45 | "v2land-sails-pg-session": "^1.0.4" 46 | }, 47 | "scripts": { 48 | "inspect": "node --inspect app.js", 49 | "start": "node app.js", 50 | "migrate": "db-migrate", 51 | "postinstall": "bower install", 52 | "eslint": "eslint ./*.js api assets/js config migrations tasks", 53 | "stylelint": "stylelint assets/styles/*.*" 54 | }, 55 | "main": "app.js", 56 | "author": { 57 | "name": "Arek Sredzki", 58 | "email": "arek@sredzki.com", 59 | "url": "https://arek.io/" 60 | }, 61 | "license": "MIT", 62 | "devDependencies": { 63 | "eslint": "^8.30.0", 64 | "stylelint": "^14.16.0", 65 | "stylelint-config-standard": "^29.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/wait.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | # OSX doesn't have the timeout command but it can be installed using homebrew. 5 | # Check if the gtimeout command exists and, if needed, tell the user how to 6 | # install it. 7 | timeout_cmd="timeout" 8 | if [ $(uname -s) = "Darwin" ]; then 9 | timeout_cmd="gtimeout" 10 | if ! which $timeout_cmd 2>&1 >/dev/null; then 11 | echo "Can't find the command 'gtimeout'. Run 'brew install coreutils' to install it." 12 | exit 1 13 | fi 14 | fi 15 | 16 | cmdname=$(basename $0) 17 | 18 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 19 | 20 | usage() 21 | { 22 | cat << USAGE >&2 23 | Usage: 24 | $cmdname host:port [-s] [-t timeout] [-- command args] 25 | -h HOST | --host=HOST Host or IP under test 26 | -p PORT | --port=PORT TCP port under test 27 | Alternatively, you specify the host and port as host:port 28 | -s | --strict Only execute subcommand if the test succeeds 29 | -q | --quiet Don\'t output any status messages 30 | -t TIMEOUT | --timeout=TIMEOUT 31 | Timeout in seconds, zero for no timeout 32 | -- COMMAND ARGS Execute command with args after the test finishes 33 | USAGE 34 | exit 1 35 | } 36 | 37 | wait_for() 38 | { 39 | if [[ $TIMEOUT -gt 0 ]]; then 40 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 41 | else 42 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 43 | fi 44 | start_ts=$(date +%s) 45 | while : 46 | do 47 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 48 | result=$? 49 | if [[ $result -eq 0 ]]; then 50 | end_ts=$(date +%s) 51 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 52 | break 53 | fi 54 | sleep 1 55 | done 56 | return $result 57 | } 58 | 59 | wait_for_wrapper() 60 | { 61 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 62 | if [[ $QUIET -eq 1 ]]; then 63 | $timeout_cmd $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 64 | else 65 | $timeout_cmd $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 66 | fi 67 | PID=$! 68 | trap "kill -INT -$PID" INT 69 | wait $PID 70 | RESULT=$? 71 | if [[ $RESULT -ne 0 ]]; then 72 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 73 | fi 74 | return $RESULT 75 | } 76 | 77 | # process arguments 78 | while [[ $# -gt 0 ]] 79 | do 80 | case "$1" in 81 | *:* ) 82 | hostport=(${1//:/ }) 83 | HOST=${hostport[0]} 84 | PORT=${hostport[1]} 85 | shift 1 86 | ;; 87 | --child) 88 | CHILD=1 89 | shift 1 90 | ;; 91 | -q | --quiet) 92 | QUIET=1 93 | shift 1 94 | ;; 95 | -s | --strict) 96 | STRICT=1 97 | shift 1 98 | ;; 99 | -h) 100 | HOST="$2" 101 | if [[ $HOST == "" ]]; then break; fi 102 | shift 2 103 | ;; 104 | --host=*) 105 | HOST="${1#*=}" 106 | shift 1 107 | ;; 108 | -p) 109 | PORT="$2" 110 | if [[ $PORT == "" ]]; then break; fi 111 | shift 2 112 | ;; 113 | --port=*) 114 | PORT="${1#*=}" 115 | shift 1 116 | ;; 117 | -t) 118 | TIMEOUT="$2" 119 | if [[ $TIMEOUT == "" ]]; then break; fi 120 | shift 2 121 | ;; 122 | --timeout=*) 123 | TIMEOUT="${1#*=}" 124 | shift 1 125 | ;; 126 | --) 127 | shift 128 | CLI="$@" 129 | break 130 | ;; 131 | --help) 132 | usage 133 | ;; 134 | *) 135 | echoerr "Unknown argument: $1" 136 | usage 137 | ;; 138 | esac 139 | done 140 | 141 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 142 | echoerr "Error: you need to provide a host and port to test." 143 | usage 144 | fi 145 | 146 | TIMEOUT=${TIMEOUT:-15} 147 | STRICT=${STRICT:-0} 148 | CHILD=${CHILD:-0} 149 | QUIET=${QUIET:-0} 150 | 151 | if [[ $CHILD -gt 0 ]]; then 152 | wait_for 153 | RESULT=$? 154 | exit $RESULT 155 | else 156 | if [[ $TIMEOUT -gt 0 ]]; then 157 | wait_for_wrapper 158 | RESULT=$? 159 | else 160 | wait_for 161 | RESULT=$? 162 | fi 163 | fi 164 | 165 | if [[ $CLI != "" ]]; then 166 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 167 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 168 | exit $RESULT 169 | fi 170 | exec $CLI 171 | else 172 | exit $RESULT 173 | fi 174 | -------------------------------------------------------------------------------- /tasks/README.md: -------------------------------------------------------------------------------- 1 | # About the `tasks` folder 2 | 3 | The `tasks` directory is a suite of Grunt tasks and their configurations, bundled for your convenience. The Grunt integration is mainly useful for bundling front-end assets, (like stylesheets, scripts, & markup templates) but it can also be used to run all kinds of development tasks, from browserify compilation to database migrations. 4 | 5 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, read on! 6 | 7 | 8 | ### How does this work? 9 | 10 | The asset pipeline bundled in Sails is a set of Grunt tasks configured with conventional defaults designed to make your project more consistent and productive. 11 | 12 | The entire front-end asset workflow in Sails is completely customizable-- while it provides some suggestions out of the box, Sails makes no pretense that it can anticipate all of the needs you'll encounter building the browser-based/front-end portion of your application. Who's to say you're even building an app for a browser? 13 | 14 | 15 | 16 | ### What tasks does Sails run automatically? 17 | 18 | Sails runs some of these tasks (the ones in the `tasks/register` folder) automatically when you run certain commands. 19 | 20 | ###### `sails lift` 21 | 22 | Runs the `default` task (`tasks/register/default.js`). 23 | 24 | ###### `sails lift --prod` 25 | 26 | Runs the `prod` task (`tasks/register/prod.js`). 27 | 28 | ###### `sails www` 29 | 30 | Runs the `build` task (`tasks/register/build.js`). 31 | 32 | ###### `sails www --prod` (production) 33 | 34 | Runs the `buildProd` task (`tasks/register/buildProd.js`). 35 | 36 | 37 | ### Can I customize this for SASS, Angular, client-side Pug templates, etc? 38 | 39 | You can modify, omit, or replace any of these Grunt tasks to fit your requirements. You can also add your own Grunt tasks- just add a `someTask.js` file in the `grunt/config` directory to configure the new task, then register it with the appropriate parent task(s) (see files in `grunt/register/*.js`). 40 | 41 | 42 | ### Do I have to use Grunt? 43 | 44 | Nope! To disable Grunt integration in Sails, just delete your Gruntfile or disable the Grunt hook. 45 | 46 | 47 | ### What if I'm not building a web frontend? 48 | 49 | That's ok! A core tenant of Sails is client-agnosticism-- it's especially designed for building APIs used by all sorts of clients; native Android/iOS/Cordova, serverside SDKs, etc. 50 | 51 | You can completely disable Grunt by following the instructions above. 52 | 53 | If you still want to use Grunt for other purposes, but don't want any of the default web front-end stuff, just delete your project's `assets` folder and remove the front-end oriented tasks from the `grunt/register` and `grunt/config` folders. You can also run `sails new myCoolApi --no-frontend` to omit the `assets` folder and front-end-oriented Grunt tasks for future projects. You can also replace your `sails-generate-frontend` module with alternative community generators, or create your own. This allows `sails new` to create the boilerplate for native iOS apps, Android apps, Cordova apps, SteroidsJS apps, etc. 54 | 55 | -------------------------------------------------------------------------------- /tasks/config/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This grunt task is configured to clean out the contents in the .tmp/public of your 7 | * sails project. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('clean', { 15 | dev: ['.tmp/public/**'], 16 | build: ['www'] 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-contrib-clean'); 20 | }; 21 | -------------------------------------------------------------------------------- /tasks/config/coffee.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile CoffeeScript files to JavaScript. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compiles coffeeScript files from `assest/js` into Javascript and places them into 7 | * `.tmp/public/js` directory. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-coffee 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('coffee', { 15 | dev: { 16 | options: { 17 | bare: true, 18 | sourceMap: true, 19 | sourceRoot: './' 20 | }, 21 | files: [{ 22 | expand: true, 23 | cwd: 'assets/js/', 24 | src: ['**/*.coffee'], 25 | dest: '.tmp/public/js/', 26 | ext: '.js' 27 | }, { 28 | expand: true, 29 | cwd: 'assets/js/', 30 | src: ['**/*.coffee'], 31 | dest: '.tmp/public/js/', 32 | ext: '.js' 33 | }] 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-coffee'); 38 | }; 39 | -------------------------------------------------------------------------------- /tasks/config/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Concatenate files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Concatenates files javascript and css from a defined array. Creates concatenated files in 7 | * .tmp/public/contact directory 8 | * [concat](https://github.com/gruntjs/grunt-contrib-concat) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-concat 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('concat', { 16 | js: { 17 | src: require('../pipeline').jsFilesToInject, 18 | dest: '.tmp/public/concat/production.js' 19 | }, 20 | css: { 21 | src: require('../pipeline').cssFilesToInject, 22 | dest: '.tmp/public/concat/production.css' 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-concat'); 27 | }; 28 | -------------------------------------------------------------------------------- /tasks/config/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * # dev task config 7 | * Copies all directories and files, exept coffescript and less fiels, from the sails 8 | * assets folder into the .tmp/public directory. 9 | * 10 | * # build task config 11 | * Copies all directories nd files from the .tmp/public directory into a www directory. 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-copy 15 | */ 16 | module.exports = function(grunt) { 17 | 18 | grunt.config.set('copy', { 19 | dev: { 20 | files: [{ 21 | expand: true, 22 | cwd: './assets', 23 | src: ['**/*.!(coffee|less)'], 24 | dest: '.tmp/public' 25 | }] 26 | }, 27 | build: { 28 | files: [{ 29 | expand: true, 30 | cwd: '.tmp/public', 31 | src: ['**/*'], 32 | dest: 'www' 33 | }] 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-copy'); 38 | }; 39 | -------------------------------------------------------------------------------- /tasks/config/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compress CSS files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies css files and places them into .tmp/public/min directory. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-cssmin 10 | */ 11 | module.exports = function(grunt) { 12 | 13 | grunt.config.set('cssmin', { 14 | dist: { 15 | src: ['.tmp/public/concat/production.css'], 16 | dest: '.tmp/public/min/production.min.css' 17 | } 18 | }); 19 | 20 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 21 | }; 22 | -------------------------------------------------------------------------------- /tasks/config/jst.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Precompiles Underscore templates to a `.jst` file. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * (i.e. basically it takes HTML files and turns them into tiny little 7 | * javascript functions that you pass data to and return HTML. This can 8 | * speed up template rendering on the client, and reduce bandwidth usage.) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-jst 12 | * 13 | */ 14 | 15 | module.exports = function(grunt) { 16 | 17 | grunt.config.set('jst', { 18 | dev: { 19 | 20 | // To use other sorts of templates, specify a regexp like the example below: 21 | // options: { 22 | // templateSettings: { 23 | // interpolate: /\{\{(.+?)\}\}/g 24 | // } 25 | // }, 26 | 27 | // Note that the interpolate setting above is simply an example of overwriting lodash's 28 | // default interpolation. If you want to parse templates with the default _.template behavior 29 | // (i.e. using
), there's no need to overwrite `templateSettings.interpolate`. 30 | 31 | 32 | files: { 33 | // e.g. 34 | // 'relative/path/from/gruntfile/to/compiled/template/destination' : ['relative/path/to/sourcefiles/**/*.html'] 35 | '.tmp/public/jst.js': require('../pipeline').templateFilesToInject 36 | } 37 | } 38 | }); 39 | 40 | grunt.loadNpmTasks('grunt-contrib-jst'); 41 | }; 42 | -------------------------------------------------------------------------------- /tasks/config/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compiles LESS files into CSS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Only the `assets/styles/importer.less` is compiled. 7 | * This allows you to control the ordering yourself, i.e. import your 8 | * dependencies, mixins, variables, resets, etc. before other stylesheets) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-less 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('less', { 16 | dev: { 17 | files: [{ 18 | expand: true, 19 | cwd: 'assets/styles/', 20 | src: ['importer.less'], 21 | dest: '.tmp/public/styles/', 22 | ext: '.css' 23 | }] 24 | } 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-contrib-less'); 28 | }; 29 | -------------------------------------------------------------------------------- /tasks/config/pug.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.config.set('pug', { 4 | dev: { 5 | options: {}, 6 | files: [{ 7 | expand: true, 8 | cwd: 'assets/', 9 | src: '**/*.pug', 10 | dest: '.tmp/public/', 11 | ext: '.html' 12 | }] 13 | } 14 | }); 15 | 16 | grunt.loadNpmTasks('grunt-contrib-pug'); 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/config/sass.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.config.set('sass', { 4 | dev: { 5 | options: { 6 | implementation: require('node-sass'), 7 | // style: 'compressed', 8 | }, 9 | files: [{ 10 | expand: true, 11 | cwd: 'assets/styles/', 12 | src: ['importer.scss'], 13 | dest: '.tmp/public/styles/', 14 | ext: '.css' 15 | }] 16 | } 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-sass'); 20 | }; 21 | -------------------------------------------------------------------------------- /tasks/config/sync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A grunt task to keep directories in sync. It is very similar to grunt-contrib-copy 3 | * but tries to copy only those files that has actually changed. 4 | * 5 | * --------------------------------------------------------------- 6 | * 7 | * Synchronize files from the `assets` folder to `.tmp/public`, 8 | * smashing anything that's already there. 9 | * 10 | * For usage docs see: 11 | * https://github.com/tomusdrw/grunt-sync 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | 16 | grunt.config.set('sync', { 17 | dev: { 18 | files: [{ 19 | cwd: './assets', 20 | src: ['**/*.!(coffee)'], 21 | dest: '.tmp/public' 22 | }] 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-sync'); 27 | }; 28 | -------------------------------------------------------------------------------- /tasks/config/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minify files with UglifyJS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies client-side javascript `assets`. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-uglify 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('uglify', { 15 | dist: { 16 | src: ['.tmp/public/concat/production.js'], 17 | dest: '.tmp/public/min/production.min.js' 18 | } 19 | }); 20 | 21 | grunt.loadNpmTasks('grunt-contrib-uglify-es'); 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/config/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run predefined tasks whenever watched file patterns are added, changed or deleted. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Watch for changes on 7 | * - files in the `assets` folder 8 | * - the `tasks/pipeline.js` file 9 | * and re-run the appropriate tasks. 10 | * 11 | * For usage docs see: 12 | * https://github.com/gruntjs/grunt-contrib-watch 13 | * 14 | */ 15 | module.exports = function(grunt) { 16 | 17 | grunt.config.set('watch', { 18 | api: { 19 | 20 | // API files to watch: 21 | files: ['api/**/*', '!**/node_modules/**'] 22 | }, 23 | assets: { 24 | 25 | // Assets to watch: 26 | files: ['assets/**/*', 'tasks/pipeline.js', '!**/node_modules/**'], 27 | 28 | // When assets are changed: 29 | tasks: ['syncAssets' , 'linkAssets'] 30 | } 31 | }); 32 | 33 | grunt.loadNpmTasks('grunt-contrib-watch'); 34 | }; 35 | -------------------------------------------------------------------------------- /tasks/config/wiredep.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config.set('wiredep', { 3 | task: { 4 | // Point to the files that should be updated when 5 | // you run 'grunt wiredep' 6 | src: [ 7 | 'views/**/*.pug', // .pug support... 8 | ], 9 | 10 | // We need this line so injection is correct path 11 | ignorePath: '../assets', 12 | 13 | options: { 14 | // See wiredep's configuration documentation for the options 15 | // you may pass: 16 | 17 | // https://github.com/taptapship/wiredep#configuration 18 | } 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-wiredep'); 23 | }; 24 | -------------------------------------------------------------------------------- /tasks/pipeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt/pipeline.js 3 | * 4 | * The order in which your css, javascript, and template files should be 5 | * compiled and linked from your views and static HTML files. 6 | * 7 | * (Note that you can take advantage of Grunt-style wildcard/glob/splat expressions 8 | * for matching multiple files.) 9 | */ 10 | 11 | 12 | 13 | // CSS files to inject in order 14 | // 15 | // (if you're using LESS with the built-in default config, you'll want 16 | // to change `assets/styles/importer.less` instead.) 17 | var cssFilesToInject = [ 18 | 'styles/**/*.css' 19 | ]; 20 | 21 | 22 | // Client-side javascript files to inject in order 23 | // (uses Grunt-style wildcard/glob/splat expressions) 24 | var jsFilesToInject = [ 25 | 26 | 'js/main.js', 27 | 28 | // All of the rest of your client-side js files 29 | // will be injected here in no particular order. 30 | 'js/**/*.js' 31 | ]; 32 | 33 | 34 | // Client-side HTML templates are injected using the sources below 35 | // The ordering of these templates shouldn't matter. 36 | // (uses Grunt-style wildcard/glob/splat expressions) 37 | // 38 | // By default, Sails uses JST templates and precompiles them into 39 | // functions for you. If you want to use pug, handlebars, dust, etc., 40 | // with the linker, no problem-- you'll just want to make sure the precompiled 41 | // templates get spit out to the same file. Be sure and check out `tasks/README.md` 42 | // for information on customizing and installing new tasks. 43 | var templateFilesToInject = [ 44 | 'templates/**/*.html' 45 | ]; 46 | 47 | 48 | 49 | // Prefix relative paths to source files so they point to the proper locations 50 | // (i.e. where the other Grunt tasks spit them out, or in some cases, where 51 | // they reside in the first place) 52 | module.exports.cssFilesToInject = cssFilesToInject.map(function(path) { 53 | return '.tmp/public/' + path; 54 | }); 55 | module.exports.jsFilesToInject = jsFilesToInject.map(function(path) { 56 | return '.tmp/public/' + path; 57 | }); 58 | module.exports.templateFilesToInject = templateFilesToInject.map(function(path) { 59 | return 'assets/' + path; 60 | }); 61 | -------------------------------------------------------------------------------- /tasks/register/build.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('build', [ 3 | 'compileAssets', 4 | 'linkAssetsBuild', 5 | 'clean:build', 6 | 'copy:build' 7 | ]); 8 | }; 9 | -------------------------------------------------------------------------------- /tasks/register/buildProd.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('buildProd', [ 3 | 'compileAssets', 4 | 'concat', 5 | 'uglify', 6 | 'cssmin', 7 | 'linkAssetsBuildProd', 8 | 'clean:build', 9 | 'copy:build' 10 | ]); 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/register/compileAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('compileAssets', [ 3 | 'clean:dev', 4 | 'jst:dev', 5 | 'pug:dev', 6 | 'less:dev', 7 | 'sass:dev', 8 | 'copy:dev', 9 | 'coffee:dev' 10 | ]); 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/register/default.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.registerTask('default', [ 3 | 'wiredep', 4 | 'compileAssets', 5 | 'linkAssets', 6 | 'watch' 7 | ]); 8 | }; 9 | -------------------------------------------------------------------------------- /tasks/register/linkAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssets', [ 3 | 'sails-linker:devJs', 4 | 'sails-linker:devStyles', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:devJsPug', 7 | 'sails-linker:devStylesPug', 8 | 'sails-linker:devTplPug' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuild.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssetsBuild', [ 3 | 'sails-linker:devJsRelative', 4 | 'sails-linker:devStylesRelative', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:devJsRelativePug', 7 | 'sails-linker:devStylesRelativePug', 8 | 'sails-linker:devTplPug' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuildProd.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssetsBuildProd', [ 3 | 'sails-linker:prodJsRelative', 4 | 'sails-linker:prodStylesRelative', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:prodJsRelativePug', 7 | 'sails-linker:prodStylesRelativePug', 8 | 'sails-linker:devTplPug' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('prod', [ 3 | 'compileAssets', 4 | 'concat', 5 | 'uglify', 6 | 'cssmin', 7 | 'sails-linker:prodJs', 8 | 'sails-linker:prodStyles', 9 | 'sails-linker:devTpl', 10 | 'sails-linker:prodJsPug', 11 | 'sails-linker:prodStylesPug', 12 | 'sails-linker:devTplPug' 13 | ]); 14 | }; 15 | -------------------------------------------------------------------------------- /tasks/register/syncAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('syncAssets', [ 3 | 'jst:dev', 4 | 'pug:dev', 5 | 'less:dev', 6 | 'sass:dev', 7 | 'sync:dev', 8 | 'coffee:dev' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /views/403.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Unexpected Error Occurred 5 | 6 | // STYLES 7 | link(rel="stylesheet", href="/styles/importer.css") 8 | // STYLES END 9 | body 10 | .header 11 | .navbar.navbar-default(role='navigation') 12 | .container-fluid 13 | .navbar-header 14 | a.pull-left(href='/') 15 | img.navbar-logo(src=process.env.WEBSITE_NAV_LOGO || '/images/logo.svg') 16 | a.navbar-brand(href='/') #{process.env.WEBSITE_APP_TITLE || 'Electron App'} 17 | .collapse.navbar-collapse 18 | ul.nav.navbar-nav 19 | li 20 | a(href='/') Home 21 | auth-toolbar 22 | .container 23 | .jumbotron 24 | h1 25 | i.fa.fa-ban.red 26 | | 403 27 | p.lead 28 | | Sorry! You don't have access permissions for this page. 29 | p 30 | a.btn.btn-default.btn-lg.green(href='/') 31 | | Take Me To The Homepage 32 | .container 33 | .body-content 34 | .row 35 | .col-md-6 36 | h2 What happened? 37 | p.lead 38 | | A 403 error status indicates that you don't have permission to access the file or page. In general, web servers and websites have directories and files that are not open to the public web for security reasons. 39 | .col-md-6 40 | h2 What can I do? 41 | p.lead If you're a site vistor 42 | p 43 | | Please use your browsers back button and check that you're in the right place. If you need immediate assistance, please send us an email instead. 44 | p.lead If you're the site owner 45 | p 46 | | Please check that you're in the right place and get in touch with your website provider if you believe this to be an error. 47 | -------------------------------------------------------------------------------- /views/404.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Page not found 5 | 6 | // STYLES 7 | link(rel="stylesheet", href="/styles/importer.css") 8 | // STYLES END 9 | body 10 | .header 11 | .navbar.navbar-default(role='navigation') 12 | .container-fluid 13 | .navbar-header 14 | a.pull-left(href='/') 15 | img.navbar-logo(src=process.env.WEBSITE_NAV_LOGO || '/images/logo.svg') 16 | a.navbar-brand(href='/') #{process.env.WEBSITE_APP_TITLE || 'Electron App'} 17 | .collapse.navbar-collapse 18 | ul.nav.navbar-nav 19 | li 20 | a(href='/') Home 21 | auth-toolbar 22 | .container 23 | // Jumbotron 24 | .jumbotron 25 | h1 26 | span.glyphicon.glyphicon-fire.red 27 | | 404 28 | p.lead 29 | | The requested page is not available. 30 | a.btn.btn-default.btn-lg.text-center(href='/') 31 | span.green Go Home 32 | -------------------------------------------------------------------------------- /views/500.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Unexpected Error Occurred 5 | 6 | // STYLES 7 | link(rel="stylesheet", href="/styles/importer.css") 8 | // STYLES END 9 | body 10 | .header 11 | .navbar.navbar-default(role='navigation') 12 | .container-fluid 13 | .navbar-header 14 | a.pull-left(href='/') 15 | img.navbar-logo(src=process.env.WEBSITE_NAV_LOGO || '/images/logo.svg') 16 | a.navbar-brand(href='/') #{process.env.WEBSITE_APP_TITLE || 'Electron App'} 17 | .collapse.navbar-collapse 18 | ul.nav.navbar-nav 19 | li 20 | a(href='/') Home 21 | auth-toolbar 22 | .container 23 | // Jumbotron 24 | .jumbotron 25 | h1 26 | span.glyphicon.glyphicon-fire.red 27 | | 500 28 | p.lead 29 | | The web server is returning an internal error. 30 | a.btn.btn-default.btn-lg.text-center(href='javascript:document.location.reload(true);') 31 | span.green Try This Page Again 32 | .container 33 | .body-content 34 | .row 35 | .col-md-6 36 | h2 What happened? 37 | p.lead 38 | | A 500 error status implies there is a problem with the web server's software causing it to malfunction. 39 | .col-md-6 40 | h2 What can I do? 41 | p.lead If you're a site vistor 42 | p 43 | | Nothing you can do at the moment. If you need immediate assistance, please send us an email instead. We apologize for any inconvenience. 44 | p.lead If you're the site owner 45 | p 46 | | This error can only be fixed by server admins, please contact your website provider. 47 | -------------------------------------------------------------------------------- /views/homepage.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | title #{process.env.WEBSITE_TITLE || 'Awesome Electron Desktop App'} 6 | meta(name='description', content='Download the interactive app for doing whatever it does.') 7 | meta(name='author', content='Arek Sredzki') 8 | meta(name='viewport', content='width=device-width, initial-scale=1') 9 | base(href="/") 10 | 11 | // bower:css 12 | link(rel='stylesheet', href='/bower_components/angular-ui-notification/dist/angular-ui-notification.css') 13 | link(rel='stylesheet', href='/bower_components/components-font-awesome/css/font-awesome.css') 14 | // endbower 15 | 16 | // STYLES 17 | link(rel="stylesheet", href="/styles/importer.css") 18 | // STYLES END 19 | body(ng-app='app', ng-controller='MainController as main') 20 | .header 21 | .navbar.navbar-default(role='navigation') 22 | .container-fluid 23 | .navbar-header 24 | a.pull-left(href='/') 25 | img.navbar-logo(src=process.env.WEBSITE_NAV_LOGO || '/images/logo.svg') 26 | a.navbar-brand(href='/') #{process.env.WEBSITE_APP_TITLE || 'Electron App'} 27 | .collapse.navbar-collapse 28 | ul.nav.navbar-nav 29 | li(ng-class="{ active: isActive('/')}") 30 | a(href='/') Home 31 | ul.nav.navbar-nav 32 | li(ng-class="{ active: isActive('/releases')}") 33 | a(href='/releases') Download 34 | auth-toolbar 35 | .container 36 | div(ng-view='') 37 | .footer 38 | p 39 | i.fa.fa-fw.fa-code 40 | | with 41 | i.fa.fa-fw.fa-heart 42 | | by Arek Sredzki 43 | 44 | 45 | // bower:js 46 | script(src='/bower_components/angular/angular.js') 47 | script(src='/bower_components/jquery/dist/jquery.js') 48 | script(src='/bower_components/angular-animate/angular-animate.js') 49 | script(src='/bower_components/angular-messages/angular-messages.js') 50 | script(src='/bower_components/angular-route/angular-route.js') 51 | script(src='/bower_components/angular-sanitize/angular-sanitize.js') 52 | script(src='/bower_components/angular-xeditable/dist/js/xeditable.min.js') 53 | script(src='/bower_components/lodash/lodash.js') 54 | script(src='/bower_components/angular-ui-notification/dist/angular-ui-notification.js') 55 | script(src='/bower_components/angular-bootstrap/ui-bootstrap-tpls.js') 56 | script(src='/bower_components/angular-confirm-modal/angular-confirm.min.js') 57 | script(src='/bower_components/angular-jwt/dist/angular-jwt.js') 58 | script(src='/bower_components/ngstorage/ngStorage.js') 59 | script(src='/bower_components/sails.io.js/dist/sails.io.js') 60 | script(src='/bower_components/angular-sails/dist/angular-sails.js') 61 | script(src='/bower_components/moment/moment.js') 62 | script(src='/bower_components/angular-moment/angular-moment.js') 63 | script(src='/bower_components/ng-file-upload/ng-file-upload.js') 64 | script(src='/bower_components/angular-PubSub/src/angular-pubsub.js') 65 | script(src='/bower_components/re-tree/re-tree.js') 66 | script(src='/bower_components/ng-device-detector/ng-device-detector.js') 67 | script(src='/bower_components/compare-versions/index.js') 68 | // endbower 69 | 70 | // SCRIPTS 71 | script(src="/js/main.js") 72 | script(src="/js/admin/add-flavor-modal/add-flavor-modal-controller.js") 73 | script(src="/js/admin/add-version-asset-modal/add-version-asset-modal-controller.js") 74 | script(src="/js/admin/add-version-modal/add-version-modal-controller.js") 75 | script(src="/js/admin/admin.js") 76 | script(src="/js/admin/edit-version-asset-modal/edit-version-asset-modal-controller.js") 77 | script(src="/js/admin/edit-version-modal/edit-version-modal-controller.js") 78 | script(src="/js/admin/version-table/version-table-controller.js") 79 | script(src="/js/core/auth/auth-service.js") 80 | script(src="/js/core/auth/auth.js") 81 | script(src="/js/core/auth/login/login-controller.js") 82 | script(src="/js/core/auth/logout/logout-controller.js") 83 | script(src="/js/core/core.js") 84 | script(src="/js/core/data/data-service.js") 85 | script(src="/js/core/data/data.js") 86 | script(src="/js/core/dependencies/dependencies.js") 87 | script(src="/js/core/nav-controller.js") 88 | script(src="/js/download/download-controller.js") 89 | script(src="/js/home/home-controller.js") 90 | // SCRIPTS END 91 | --------------------------------------------------------------------------------