├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.cn.md ├── README.md ├── core ├── const.js ├── services │ ├── account-manager.js │ ├── client-manager.js │ ├── collaborators.js │ ├── datacenter-manager.js │ ├── deployments.js │ ├── email-manager.js │ └── package-manager.js └── utils │ ├── common.js │ └── security.js ├── docker-compose.yml ├── docs ├── install-server-by-docker.cn.md ├── install-server-by-docker.md └── install-server.md ├── locales ├── en.json └── zh.json ├── package-lock.json ├── package.json ├── process.json ├── public ├── js │ ├── bootstrap-3.3.7 │ │ ├── css │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap-theme.min.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ └── bootstrap.min.js │ └── jquery-3.1.1.min.js └── stylesheets │ ├── common.css │ └── signin.css ├── renovate.json ├── routes ├── accessKeys.js ├── apps.js ├── index.js └── users.js ├── sql ├── codepush-all-docker.sql ├── codepush-all.sql ├── codepush-v0.2.14-patch.sql ├── codepush-v0.2.15-patch.sql ├── codepush-v0.3.0-patch.sql ├── codepush-v0.4.0-patch.sql └── codepush-v0.5.0-patch.sql ├── src ├── app.ts ├── core │ ├── app-error.ts │ ├── config.ts │ ├── const.ts │ ├── i18n.ts │ ├── middleware.ts │ ├── services │ │ ├── account-manager.ts │ │ ├── app-manager.ts │ │ ├── client-manager.ts │ │ ├── collaborators-manager.ts │ │ ├── datacenter-manager.ts │ │ ├── deployments-manager.ts │ │ ├── email-manager.ts │ │ └── package-manager.ts │ └── utils │ │ ├── common.ts │ │ ├── connections.ts │ │ ├── qetag.ts │ │ ├── security.ts │ │ └── storage.ts ├── db.ts ├── models │ ├── apps.ts │ ├── collaborators.ts │ ├── deployments.ts │ ├── deployments_history.ts │ ├── deployments_versions.ts │ ├── log_report_deploy.ts │ ├── log_report_download.ts │ ├── packages.ts │ ├── packages_diff.ts │ ├── packages_metrics.ts │ ├── user_tokens.ts │ ├── users.ts │ └── versions.ts ├── routes │ ├── accessKeys.ts │ ├── account.ts │ ├── apps.ts │ ├── auth.ts │ ├── index.ts │ ├── indexV1.ts │ └── users.ts └── www.ts ├── tests ├── api │ ├── accessKeys │ │ └── accessKeys.test.js │ ├── account │ │ └── account.test.js │ ├── apps │ │ ├── apps.test.js │ │ ├── bundle.zip │ │ ├── bundle_v2.zip │ │ └── release.test.js │ ├── auth │ │ └── auth.test.js │ ├── index │ │ └── index.test.js │ ├── init │ │ └── database.js │ └── users │ │ └── users.test.js └── unit │ └── .gitkeep ├── tsconfig.json └── views ├── auth ├── confirm.pug ├── login.pug ├── password.pug └── register.pug ├── common.pug ├── index.pug └── tokens.pug /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [{*.json,*.yml}] 12 | indent_size = 2 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=code-push-server 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@shm-open/eslint-config-bundle'], 3 | }; 4 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | services: 21 | redis: 22 | image: redis 23 | ports: 24 | - 6379:6379 25 | mysql: 26 | image: mysql 27 | env: 28 | MYSQL_ROOT_PASSWORD: password 29 | ports: 30 | - 3306:3306 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Use Node.js ${{ matrix.node-version }} 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | - run: npm ci 39 | - run: npm run build --if-present 40 | - run: npm test 41 | - run: npm run lint --if-present 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | *.DS_Store 10 | bin/ 11 | .nyc_output/ 12 | 13 | # deps 14 | node_modules/ 15 | 16 | 17 | # vs code 18 | .history 19 | .idea 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | CHANGELOG.md 3 | coverage 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('@shm-open/eslint-config-bundle/prettier'); 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | ARG VERSION latest 4 | 5 | RUN npm install -g @shm-open/code-push-server@${VERSION} pm2@latest --no-optional 6 | 7 | RUN mkdir /data/ 8 | 9 | WORKDIR /data/ 10 | 11 | COPY ./process.json /data/process.json 12 | 13 | # CMD ["pm2-runtime", "/data/process.json"] 14 | # workaround for issue https://github.com/Unitech/pm2/issues/4950 15 | CMD ["sh", "-c", "pm2 ps && pm2-runtime /data/process.json"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020-present shihuimiao 4 | 5 | Copyright (c) 2016-2019 tablee 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT := $(shell pwd) 2 | VERSION := $(shell node -p "require('./package.json').version") 3 | 4 | .PHONY: test 5 | test: 6 | @echo "\nRunning integration tests..." 7 | @mocha tests/api/init --exit 8 | @mocha tests/api/users tests/api/auth tests/api/account tests/api/accessKeys tests/api/apps tests/api/index --exit --recursive --timeout 30000 9 | 10 | .PHONY: coverage 11 | coverage: 12 | @echo "\nCheck test coverage..." 13 | @mocha tests/api/init --exit 14 | @nyc mocha tests/api/users tests/api/auth tests/api/account tests/api/accessKeys tests/api/apps tests/api/index --exit --recursive --timeout 30000 15 | 16 | .PHONY: release-docker 17 | release-docker: 18 | @echo "\nBuilding docker image..." 19 | docker pull node:lts-alpine 20 | docker build --build-arg VERSION=${VERSION} -t shmopen/code-push-server:latest --no-cache . 21 | docker tag shmopen/code-push-server:latest shmopen/code-push-server:${VERSION} 22 | docker push shmopen/code-push-server:${VERSION} 23 | docker push shmopen/code-push-server:latest 24 | -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # CodePush 服务端 2 | 3 | 微软官方的 CodePush 在国内的网络访问较慢, 所以我们使用这个服务端来架设自己的 CodePush 服务 4 | 5 | ## 关于本项目 6 | 7 | 因为原 [code-push-server](https://github.com/lisong/code-push-server) 项目的作者没有积极维护了, 我们创建了这个项目用来: 8 | 9 | - 保持依赖更新 10 | - 修复任何与最新的客户端的兼容问题 11 | - 我们只使用官方的 react-native-code-push 客户端, 所以定制的功能, 比如 [is_use_diff_text](https://github.com/lisong/code-push-server#advance-feature) 会被放弃. 12 | - 我们只在生产环境使用了 react-native-code-push, 对于其他的 CodePush 客户端, 大部分功能应该没有差别, 如果遇到任何问题的话, 都欢迎提交 issue 或者 PR. 13 | 14 | ## 支持的存储方式 15 | 16 | - local: 在本地硬盘存储包文件 17 | - qiniu: 在[七牛云](http://www.qiniu.com/)存储包文件 18 | - s3: 在[aws](https://aws.amazon.com/)存储包文件 19 | - oss: 在[阿里云](https://www.aliyun.com/product/oss)存储包文件 20 | - tencentcloud: 在[腾迅云](https://cloud.tencent.com/product/cos)存储包文件 21 | 22 | ## 正确使用 code-push 热更新 23 | 24 | - 苹果 App 允许使用热更新[Apple's developer agreement](https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf), 为了不影响用户体验,规定必须使用静默更新。 Google Play 不能使用静默更新,必须弹框告知用户 App 有更新。中国的 android 市场必须采用静默更新(如果弹框提示,App 会被“请上传最新版本的二进制应用包”原因驳回)。 25 | - react-native 不同平台 bundle 包不一样,在使用 code-push-server 的时候必须创建不同的应用来区分(eg. CodePushDemo-ios 和 CodePushDemo-android) 26 | - react-native-code-push 只更新资源文件,不会更新 java 和 Objective C,所以 npm 升级依赖包版本的时候,如果依赖包使用的本地化实现, 这时候必须更改应用版本号(ios 修改 Info.plist 中的 CFBundleShortVersionString, android 修改 build.gradle 中的 versionName), 然后重新编译 app 发布到应用商店。 27 | - 推荐使用 code-push release-react 命令发布应用,该命令合并了打包和发布命令(eg. code-push release-react CodePushDemo-ios ios -d Production) 28 | - 每次向 App Store 提交新的版本时,也应该基于该提交版本同时向 code-push-server 发布一个初始版本。(因为后面每次向 code-push-server 发布版本时,code-puse-server 都会和初始版本比较,生成补丁版本) 29 | 30 | ### CodePush 命令行 31 | 32 | [code-push-cli](https://github.com/shm-open/code-push-cli) 是用来管理 App 以及发布 CodePush 版本的, 请查看命令行项目的说明了解更多 33 | 34 | ### 客户端 35 | 36 | - [React Native](https://github.com/Microsoft/react-native-code-push) 37 | - [Cordova](https://github.com/microsoft/cordova-plugin-code-push) 38 | - [Capacitor](https://github.com/mapiacompany/capacitor-codepush) 39 | 40 | ## 如何安装 code-push-server 41 | 42 | - [Docker](./docs/install-server-by-docker.cn.md) (推荐) 43 | - [直接安装](./docs/install-server.md) 44 | 45 | ## 默认帐号和密码 46 | 47 | - 帐号: `admin` 48 | - 密码: `123456` 49 | 50 | ## 常见问题 51 | 52 | - [修改密码](https://github.com/lisong/code-push-server/issues/43) 53 | - [code-push-server 使用+一些需要注意的地方](https://github.com/lisong/code-push-server/issues/135) 54 | - 支持的 targetBinaryVersion 55 | - `*` 56 | - `1.2.3` 57 | - `1.2`/`1.2.*` 58 | - `1.2.3 - 1.2.7` 59 | - `>=1.2.3 <1.2.7` 60 | - `~1.2.3` 61 | - `^1.2.3` 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodePush Server 2 | 3 | CodePush Server is a backend that manages distribution of "hot deployments" or "over the air updates" for Cordova and React Native apps. Microsoft AppCenter has dropped support for CodePush on new Cordova & React apps already, and will discontinue support for existing apps by April, 2022. This software will allow you to host your own. 4 | 5 | ## Supported Storage Options 6 | 7 | - local _storage bundle file in local machine_ 8 | - s3 _storage bundle file in [aws](https://aws.amazon.com/)_ 9 | - qiniu _storage bundle file in [qiniu](http://www.qiniu.com/)_ 10 | - oss _storage bundle file in [aliyun](https://www.aliyun.com/product/oss)_ 11 | - tencentcloud _storage bundle file in [tencentcloud](https://cloud.tencent.com/product/cos)_ 12 | 13 | ## Correct use of code-push hot update 14 | 15 | - Both Google's and Apple's developer agreements allow the use of "hot" or "OTA" updates. 16 | - The OS bundles are different. When using code-push-server, you must create different applications to distinguish them (eg. MyApp-ios and MyApp-android) 17 | - The code-push app plugins only update resource files (i.e. HTML, JavaScript, CSS, images), not native code, plugins, version number, or other meta-data. So, if any of those things change, you must resubmit to the app stores. 18 | - Every time a new version is submitted to the App Store, an initial version should also be released to code-push-server based on the submitted version. Because every time a version is released to code-push-server later, code-push-server will compare with the initial version and generate a patch version. 19 | 20 | ## Clients 21 | 22 | ### Cordova 23 | [cordova-plugin-code-push](https://github.com/byronigoe/cordova-plugin-code-push) 24 | 25 | In config.xml, add reference to your own server: 26 | ```xml 27 | 28 | 29 | ``` 30 | 31 | ### React 32 | 33 | TBD 34 | 35 | ## How to Install 36 | 37 | - [Follow the instructions here](https://github.com/byronigoe/code-push-server/blob/master/docs/install-server.md) 38 | 39 | ## Accounts 40 | 41 | The default account, setup by the database initialization is: 42 | - username: `admin` 43 | - password: `123456` 44 | 45 | Create your own account by visiting https://your-server.com/auth/register (in config.js make sure common.allowRegistration is set to true) 46 | 47 | ## How to Use 48 | 49 | - [normal](https://github.com/lisong/code-push-server/blob/master/docs/react-native-code-push.md) 50 | - [react-native-code-push](https://github.com/Microsoft/react-native-code-push) 51 | - [code-push](https://github.com/Microsoft/code-push) 52 | 53 | ## Issues 54 | 55 | [code-push-server normal solution](https://github.com/lisong/code-push-server/issues/135) 56 | 57 | [An unknown error occurred](https://github.com/lisong/code-push-server/issues?utf8=%E2%9C%93&q=unknown) 58 | 59 | [modify password](https://github.com/lisong/code-push-server/issues/43) 60 | 61 | # Feature Roadmap 62 | 63 | - [modify password](https://github.com/lisong/code-push-server/issues/43) 64 | - [code-push-server normal solution (CN)](https://github.com/lisong/code-push-server/issues/135) 65 | - targetBinaryVersion support 66 | - `*` 67 | - `1.2.3` 68 | - `1.2`/`1.2.*` 69 | - `1.2.3 - 1.2.7` 70 | - `>=1.2.3 <1.2.7` 71 | - `~1.2.3` 72 | - `^1.2.3` 73 | -------------------------------------------------------------------------------- /core/const.js: -------------------------------------------------------------------------------- 1 | function define(name, value) { 2 | Object.defineProperty(exports, name, { 3 | value: value, 4 | enumerable: true, 5 | }); 6 | } 7 | 8 | //定义支持的平台 9 | define('IOS', 1); 10 | define('IOS_NAME', 'iOS'); 11 | define('ANDROID', 2); 12 | define('ANDROID_NAME', 'Android'); 13 | define('WINDOWS', 3); 14 | define('WINDOWS_NAME', 'Windows'); 15 | 16 | //定义支持的应用类型 17 | define('REACT_NATIVE', 1); 18 | define('REACT_NATIVE_NAME', 'React-Native'); 19 | define('CORDOVA', 2); 20 | define('CORDOVA_NAME', 'Cordova'); 21 | 22 | define('PRODUCTION', 'Production'); 23 | define('STAGING', 'Staging'); 24 | 25 | define('IS_MANDATORY_YES', 1); 26 | define('IS_MANDATORY_NO', 0); 27 | 28 | define('IS_DISABLED_YES', 1); 29 | define('IS_DISABLED_NO', 0); 30 | 31 | define('RELEASE_METHOD_PROMOTE', 'Promote'); 32 | define('RELEASE_METHOD_UPLOAD', 'Upload'); 33 | 34 | define('DEPLOYMENT_SUCCEEDED', 1); 35 | define('DEPLOYMENT_FAILED', 2); 36 | 37 | define('DIFF_MANIFEST_FILE_NAME', 'hotcodepush.json'); 38 | 39 | //文本文件是否使用google diff-match-patch 计算差异 40 | define('IS_USE_DIFF_TEXT_NO', 0); 41 | define('IS_USE_DIFF_TEXT_YES', 1); 42 | 43 | define('CURRENT_DB_VERSION', '0.5.0'); 44 | -------------------------------------------------------------------------------- /core/services/collaborators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var models = require('../../models'); 3 | var _ = require('lodash'); 4 | var AppError = require('../app-error'); 5 | 6 | var proto = (module.exports = function () { 7 | function Collaborators() {} 8 | Collaborators.__proto__ = proto; 9 | return Collaborators; 10 | }); 11 | 12 | proto.listCollaborators = function (appId) { 13 | return models.Collaborators.findAll({ where: { appid: appId } }) 14 | .then((data) => { 15 | return _.reduce( 16 | data, 17 | function (result, value, key) { 18 | (result['uids'] || (result['uids'] = [])).push(value.uid); 19 | result[value.uid] = value; 20 | return result; 21 | }, 22 | [], 23 | ); 24 | }) 25 | .then((coInfo) => { 26 | var Sequelize = require('sequelize'); 27 | return models.Users.findAll({ where: { id: { [Sequelize.Op.in]: coInfo.uids } } }).then( 28 | (data2) => { 29 | return _.reduce( 30 | data2, 31 | function (result, value, key) { 32 | var permission = ''; 33 | if (!_.isEmpty(coInfo[value.id])) { 34 | permission = coInfo[value.id].roles; 35 | } 36 | result[value.email] = { permission: permission }; 37 | return result; 38 | }, 39 | {}, 40 | ); 41 | }, 42 | ); 43 | }); 44 | }; 45 | 46 | proto.addCollaborator = function (appId, uid) { 47 | return models.Collaborators.findOne({ where: { appid: appId, uid: uid } }).then((data) => { 48 | if (_.isEmpty(data)) { 49 | return models.Collaborators.create({ 50 | appid: appId, 51 | uid: uid, 52 | roles: 'Collaborator', 53 | }); 54 | } else { 55 | throw new AppError.AppError('User is already a Collaborator'); 56 | } 57 | }); 58 | }; 59 | 60 | proto.deleteCollaborator = function (appId, uid) { 61 | return models.Collaborators.findOne({ where: { appid: appId, uid: uid } }).then((data) => { 62 | if (_.isEmpty(data)) { 63 | throw new AppError.AppError('User is not a Collaborator'); 64 | } else { 65 | return models.Collaborators.destroy({ where: { id: data.id } }); 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /core/services/datacenter-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var fs = require('fs'); 4 | var os = require('os'); 5 | var security = require('../utils/security'); 6 | var common = require('../utils/common'); 7 | const MANIFEST_FILE_NAME = 'manifest.json'; 8 | const CONTENTS_NAME = 'contents'; 9 | var AppError = require('../app-error'); 10 | var log4js = require('log4js'); 11 | var log = log4js.getLogger('cps:DataCenterManager'); 12 | var path = require('path'); 13 | 14 | var proto = (module.exports = function () { 15 | function DataCenterManager() {} 16 | DataCenterManager.__proto__ = proto; 17 | return DataCenterManager; 18 | }); 19 | 20 | proto.getDataDir = function () { 21 | var dataDir = _.get(require('../config'), 'common.dataDir', {}); 22 | if (_.isEmpty(dataDir)) { 23 | dataDir = os.tmpdir(); 24 | } 25 | return dataDir; 26 | }; 27 | 28 | proto.hasPackageStoreSync = function (packageHash) { 29 | var dataDir = this.getDataDir(); 30 | var packageHashPath = path.join(dataDir, packageHash); 31 | var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); 32 | var contentPath = path.join(packageHashPath, CONTENTS_NAME); 33 | return fs.existsSync(manifestFile) && fs.existsSync(contentPath); 34 | }; 35 | 36 | proto.getPackageInfo = function (packageHash) { 37 | if (this.hasPackageStoreSync(packageHash)) { 38 | var dataDir = this.getDataDir(); 39 | var packageHashPath = path.join(dataDir, packageHash); 40 | var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); 41 | var contentPath = path.join(packageHashPath, CONTENTS_NAME); 42 | return this.buildPackageInfo(packageHash, packageHashPath, contentPath, manifestFile); 43 | } else { 44 | throw new AppError.AppError("Cannot get PackageInfo"); 45 | } 46 | }; 47 | 48 | proto.buildPackageInfo = function (packageHash, packageHashPath, contentPath, manifestFile) { 49 | return { 50 | packageHash: packageHash, 51 | path: packageHashPath, 52 | contentPath: contentPath, 53 | manifestFilePath: manifestFile, 54 | }; 55 | }; 56 | 57 | proto.validateStore = function (providePackageHash) { 58 | var dataDir = this.getDataDir(); 59 | var packageHashPath = path.join(dataDir, providePackageHash); 60 | var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); 61 | var contentPath = path.join(packageHashPath, CONTENTS_NAME); 62 | if (!this.hasPackageStoreSync(providePackageHash)) { 63 | log.debug(`validateStore providePackageHash does not exist`); 64 | return Promise.resolve(false); 65 | } 66 | return security.calcAllFileSha256(contentPath).then((manifestJson) => { 67 | var packageHash = security.packageHashSync(manifestJson); 68 | log.debug(`validateStore packageHash:`, packageHash); 69 | try { 70 | var manifestJsonLocal = JSON.parse(fs.readFileSync(manifestFile)); 71 | } catch (e) { 72 | log.debug(`validateStore manifestFile contents invalid`); 73 | return false; 74 | } 75 | var packageHashLocal = security.packageHashSync(manifestJsonLocal); 76 | log.debug(`validateStore packageHashLocal:`, packageHashLocal); 77 | if (_.eq(providePackageHash, packageHash) && _.eq(providePackageHash, packageHashLocal)) { 78 | log.debug(`validateStore store files is ok`); 79 | return true; 80 | } 81 | log.debug(`validateStore store files broken`); 82 | return false; 83 | }); 84 | }; 85 | 86 | proto.storePackage = function (sourceDst, force) { 87 | log.debug(`storePackage sourceDst:`, sourceDst); 88 | if (_.isEmpty(force)) { 89 | force = false; 90 | } 91 | var self = this; 92 | return security.calcAllFileSha256(sourceDst).then((manifestJson) => { 93 | var packageHash = security.packageHashSync(manifestJson); 94 | log.debug('storePackage manifestJson packageHash:', packageHash); 95 | var dataDir = self.getDataDir(); 96 | var packageHashPath = path.join(dataDir, packageHash); 97 | var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); 98 | var contentPath = path.join(packageHashPath, CONTENTS_NAME); 99 | return self.validateStore(packageHash).then((isValidate) => { 100 | if (!force && isValidate) { 101 | return self.buildPackageInfo( 102 | packageHash, 103 | packageHashPath, 104 | contentPath, 105 | manifestFile, 106 | ); 107 | } else { 108 | log.debug(`storePackage cover from sourceDst:`, sourceDst); 109 | return common.createEmptyFolder(packageHashPath).then(() => { 110 | return common.copy(sourceDst, contentPath).then(() => { 111 | var manifestString = JSON.stringify(manifestJson); 112 | fs.writeFileSync(manifestFile, manifestString); 113 | return self.buildPackageInfo( 114 | packageHash, 115 | packageHashPath, 116 | contentPath, 117 | manifestFile, 118 | ); 119 | }); 120 | }); 121 | } 122 | }); 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /core/services/email-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var nodemailer = require('nodemailer'); 4 | var config = require('../config'); 5 | 6 | var proto = (module.exports = function () { 7 | function EmailManager() {} 8 | EmailManager.__proto__ = proto; 9 | return EmailManager; 10 | }); 11 | 12 | proto.sendMail = function (options) { 13 | return new Promise((resolve, reject) => { 14 | if (!_.get(options, 'to')) { 15 | return reject(new AppError.AppError(`"To" is a required parameter`)); 16 | } 17 | var smtpConfig = _.get(config, 'smtpConfig'); 18 | if (!smtpConfig || !smtpConfig.host) { 19 | resolve({}); 20 | } 21 | var transporter = nodemailer.createTransport(smtpConfig); 22 | var sendEmailAddress = _.get(smtpConfig, 'auth.user'); 23 | var defaultMailOptions = { 24 | from: `"CodePush Server" <${sendEmailAddress}>`, // sender address 25 | to: '', // list of receivers 必传参数 26 | subject: 'CodePush Server', // Subject line 27 | html: '', // html body 28 | }; 29 | var mailOptions = _.assign(defaultMailOptions, options); 30 | transporter.sendMail(mailOptions, function (error, info) { 31 | if (error) { 32 | return reject(error); 33 | } 34 | resolve(info); 35 | }); 36 | }); 37 | }; 38 | 39 | proto.sendRegisterCode = function (email, code) { 40 | return proto.sendMail({ 41 | to: email, 42 | html: `
Your verification code is: ${code}
It is valid for 20 minutes.
`, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /core/utils/security.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var bcrypt = require('bcryptjs'); 3 | var crypto = require('crypto'); 4 | var fs = require('fs'); 5 | var qetag = require('../utils/qetag'); 6 | var _ = require('lodash'); 7 | var log4js = require('log4js'); 8 | var log = log4js.getLogger('cps:utils:security'); 9 | var AppError = require('../app-error'); 10 | 11 | var randToken = require('rand-token').generator({ 12 | chars: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 13 | source: 'crypto', 14 | }); 15 | var security = {}; 16 | module.exports = security; 17 | 18 | security.md5 = function (str) { 19 | var md5sum = crypto.createHash('md5'); 20 | md5sum.update(str); 21 | str = md5sum.digest('hex'); 22 | return str; 23 | }; 24 | 25 | security.passwordHashSync = function (password) { 26 | return bcrypt.hashSync(password, bcrypt.genSaltSync(12)); 27 | }; 28 | 29 | security.passwordVerifySync = function (password, hash) { 30 | return bcrypt.compareSync(password, hash); 31 | }; 32 | 33 | security.randToken = function (num) { 34 | return randToken.generate(num); 35 | }; 36 | 37 | security.parseToken = function (token) { 38 | return { identical: token.substr(-9, 9), token: token.substr(0, 28) }; 39 | }; 40 | 41 | security.fileSha256 = function (file) { 42 | return new Promise((resolve, reject) => { 43 | var rs = fs.createReadStream(file); 44 | var hash = crypto.createHash('sha256'); 45 | rs.on('data', hash.update.bind(hash)); 46 | rs.on('error', (e) => { 47 | reject(e); 48 | }); 49 | rs.on('end', () => { 50 | resolve(hash.digest('hex')); 51 | }); 52 | }); 53 | }; 54 | 55 | security.stringSha256Sync = function (contents) { 56 | var sha256 = crypto.createHash('sha256'); 57 | sha256.update(contents); 58 | return sha256.digest('hex'); 59 | }; 60 | 61 | security.packageHashSync = function (jsonData) { 62 | var sortedArr = security.sortJsonToArr(jsonData); 63 | var manifestData = _.filter(sortedArr, (v) => { 64 | return !security.isPackageHashIgnored(v.path); 65 | }).map((v) => { 66 | return v.path + ':' + v.hash; 67 | }); 68 | log.debug('packageHashSync manifestData:', manifestData); 69 | var manifestString = JSON.stringify(manifestData.sort()); 70 | manifestString = _.replace(manifestString, /\\\//g, '/'); 71 | log.debug('packageHashSync manifestString:', manifestString); 72 | return security.stringSha256Sync(manifestString); 73 | }; 74 | 75 | // The parameter is buffer or readableStream or file path 76 | security.qetag = function (buffer) { 77 | if (typeof buffer === 'string') { 78 | try { 79 | log.debug(`Check upload file ${buffer} fs.R_OK`); 80 | fs.accessSync(buffer, fs.R_OK); 81 | log.debug(`Pass upload file ${buffer}`); 82 | } catch (e) { 83 | log.error(e); 84 | return Promise.reject(new AppError.AppError(e.message)); 85 | } 86 | } 87 | log.debug(`generate file identical`); 88 | return new Promise((resolve, reject) => { 89 | qetag(buffer, (data) => { 90 | log.debug('identical:', data); 91 | resolve(data); 92 | }); 93 | }); 94 | }; 95 | 96 | security.sha256AllFiles = function (files) { 97 | return new Promise((resolve, reject) => { 98 | var results = {}; 99 | var length = files.length; 100 | var count = 0; 101 | files.forEach((file) => { 102 | security.fileSha256(file).then((hash) => { 103 | results[file] = hash; 104 | count++; 105 | if (count == length) { 106 | resolve(results); 107 | } 108 | }); 109 | }); 110 | }); 111 | }; 112 | 113 | security.uploadPackageType = function (directoryPath) { 114 | return new Promise((resolve, reject) => { 115 | var recursive = require('recursive-readdir'); 116 | var path = require('path'); 117 | var slash = require('slash'); 118 | recursive(directoryPath, (err, files) => { 119 | if (err) { 120 | log.error(new AppError.AppError(err.message)); 121 | reject(new AppError.AppError(err.message)); 122 | } else { 123 | if (files.length == 0) { 124 | log.debug(`uploadPackageType empty files`); 125 | reject(new AppError.AppError('empty files')); 126 | } else { 127 | var constName = require('../const'); 128 | const AREGEX = /android\.bundle/; 129 | const AREGEX_IOS = /main\.jsbundle/; 130 | var packageType = 0; 131 | _.forIn(files, function (value) { 132 | if (AREGEX.test(value)) { 133 | packageType = constName.ANDROID; 134 | return false; 135 | } 136 | if (AREGEX_IOS.test(value)) { 137 | packageType = constName.IOS; 138 | return false; 139 | } 140 | }); 141 | log.debug(`uploadPackageType packageType: ${packageType}`); 142 | resolve(packageType); 143 | } 144 | } 145 | }); 146 | }); 147 | }; 148 | 149 | // some files are ignored in calc hash in client sdk 150 | // https://github.com/Microsoft/react-native-code-push/pull/974/files#diff-21b650f88429c071b217d46243875987R15 151 | security.isHashIgnored = function (relativePath) { 152 | if (!relativePath) { 153 | return true; 154 | } 155 | 156 | const IgnoreMacOSX = '__MACOSX/'; 157 | const IgnoreDSStore = '.DS_Store'; 158 | 159 | return ( 160 | relativePath.startsWith(IgnoreMacOSX) || 161 | relativePath === IgnoreDSStore || 162 | relativePath.endsWith(IgnoreDSStore) 163 | ); 164 | }; 165 | 166 | security.isPackageHashIgnored = function (relativePath) { 167 | if (!relativePath) { 168 | return true; 169 | } 170 | 171 | // .codepushrelease contains code sign JWT 172 | // it should be ignored in package hash but need to be included in package manifest 173 | const IgnoreCodePushMetadata = '.codepushrelease'; 174 | return ( 175 | relativePath === IgnoreCodePushMetadata || 176 | relativePath.endsWith(IgnoreCodePushMetadata) || 177 | security.isHashIgnored(relativePath) 178 | ); 179 | }; 180 | 181 | security.calcAllFileSha256 = function (directoryPath) { 182 | return new Promise((resolve, reject) => { 183 | var recursive = require('recursive-readdir'); 184 | var path = require('path'); 185 | var slash = require('slash'); 186 | recursive(directoryPath, (error, files) => { 187 | if (error) { 188 | log.error(error); 189 | reject(new AppError.AppError(error.message)); 190 | } else { 191 | // filter files that should be ignored 192 | files = files.filter((file) => { 193 | var relative = path.relative(directoryPath, file); 194 | return !security.isHashIgnored(relative); 195 | }); 196 | 197 | if (files.length == 0) { 198 | log.debug(`calcAllFileSha256 empty files in directoryPath:`, directoryPath); 199 | reject(new AppError.AppError('empty files')); 200 | } else { 201 | security.sha256AllFiles(files).then((results) => { 202 | var data = {}; 203 | _.forIn(results, (value, key) => { 204 | var relativePath = path.relative(directoryPath, key); 205 | var matchresult = relativePath.match(/(\/|\\).*/); 206 | if (matchresult) { 207 | relativePath = path.join('CodePush', matchresult[0]); 208 | } 209 | relativePath = slash(relativePath); 210 | data[relativePath] = value; 211 | }); 212 | log.debug(`calcAllFileSha256 files:`, data); 213 | resolve(data); 214 | }); 215 | } 216 | } 217 | }); 218 | }); 219 | }; 220 | 221 | security.sortJsonToArr = function (json) { 222 | var rs = []; 223 | _.forIn(json, (value, key) => { 224 | rs.push({ path: key, hash: value }); 225 | }); 226 | return _.sortBy(rs, (o) => o.path); 227 | }; 228 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | server: 4 | image: shmopen/code-push-server:latest 5 | volumes: 6 | - data-storage:/data/storage 7 | - data-tmp:/data/tmp 8 | environment: 9 | DOWNLOAD_URL: 'http://YOUR_MACHINE_IP:3000/download' 10 | TOKEN_SECRET: 'YOUR_JWT_TOKEN_SECRET' 11 | RDS_HOST: 'mysql' 12 | RDS_USERNAME: 'codepush' 13 | RDS_PASSWORD: '123456' 14 | RDS_DATABASE: 'codepush' 15 | STORAGE_DIR: '/data/storage' 16 | DATA_DIR: '/data/tmp' 17 | NODE_ENV: 'production' 18 | REDIS_HOST: 'redis' 19 | ports: 20 | - '3000:3000' 21 | depends_on: 22 | - mysql 23 | - redis 24 | mysql: 25 | image: mysql:latest 26 | volumes: 27 | - data-mysql:/var/lib/mysql 28 | - ./sql/codepush-all-docker.sql:/docker-entrypoint-initdb.d/codepush-all.sql 29 | environment: 30 | MYSQL_ALLOW_EMPTY_PASSWORD: 'On' 31 | redis: 32 | image: redis:latest 33 | volumes: 34 | - data-redis:/data 35 | volumes: 36 | data-storage: 37 | data-tmp: 38 | data-mysql: 39 | data-redis: 40 | -------------------------------------------------------------------------------- /docs/install-server-by-docker.cn.md: -------------------------------------------------------------------------------- 1 | # docker 部署 code-push-server 2 | 3 | > 该文档用于描述 docker 部署 code-push-server,实例包含三个部分 4 | 5 | - code-push-server 部分 6 | - 更新包默认采用`local`存储(即存储在本地机器上)。使用 docker volume 存储方式,容器销毁不会导致数据丢失,除非人为删除 volume。 7 | - 内部使用 pm2 cluster 模式管理进程,默认开启进程数为 cpu 数,可以根据自己机器配置设置 docker-compose.yml 文件中 deploy 参数。 8 | - docker-compose.yml 只提供了应用的一部分参数设置,如需要设置其他配置,可以修改文件 config.js。 9 | - mysql 部分 10 | - 数据使用 docker volume 存储方式,容器销毁不会导致数据丢失,除非人为删除 volume。 11 | - 应用请勿使用 root 用户,为了安全可以创建权限相对较小的权限供 code-push-server 使用,只需要给予`select,update,insert`权限即可。初始化数据库需要使用 root 或有建表权限用户 12 | - redis 部分 13 | - `tryLoginTimes` 登录错误次数限制 14 | - `updateCheckCache` 提升应用性能 15 | - `rolloutClientUniqueIdCache` 灰度发布 16 | 17 | ## 安装 Docker 18 | 19 | 参考 Docker 官方安装教程 20 | 21 | - [>>mac 点这里](https://docs.docker.com/docker-for-mac/install/) 22 | - [>>windows 点这里](https://docs.docker.com/docker-for-windows/install/) 23 | - [>>linux 点这里](https://docs.docker.com/install/linux/docker-ce/ubuntu/) 24 | 25 | `$ docker info` 能成功输出相关信息,则安装成功,才能继续下面步骤 26 | 27 | ## 获取代码 28 | 29 | ```shell 30 | $ git clone https://github.com/shm-open/code-push-server.git 31 | $ cd code-push-server 32 | ``` 33 | 34 | ## 修改配置文件 35 | 36 | ```shell 37 | $ vim docker-compose.yml 38 | ``` 39 | 40 | _将`DOWNLOAD_URL`中`YOUR_MACHINE_IP`替换成本机外网 ip 或者域名_ 41 | 42 | ### jwt.tokenSecret 修改 43 | 44 | > code-push-server 验证登录验证方式使用的 json web token 加密方式,该对称加密算法是公开的,所以修改 config.js 中 tokenSecret 值很重要。 45 | 46 | _非常重要!非常重要! 非常重要!_ 47 | 48 | > 可以打开连接`https://www.grc.com/passwords.htm`获取 `63 random alpha-numeric characters`类型的随机生成数作为密钥 49 | 50 | _将`TOKEN_SECRET`中`YOUR_JWT_TOKEN_SECRET`替换成密钥_ 51 | 52 | ## 部署 53 | 54 | ```shell 55 | $ docker-compose up -d 56 | ``` 57 | 58 | > 如果网速不佳,需要漫长而耐心的等待。。。去和妹子聊会天吧^\_^ 59 | 60 | ## 查看进展 61 | 62 | ```shell 63 | $ docker-compose ps 64 | ``` 65 | 66 | ## 访问接口简单验证 67 | 68 | `$ curl -I http://YOUR_CODE_PUSH_SERVER_IP:3000/` 69 | 70 | 返回`200 OK` 71 | 72 | ```http 73 | HTTP/1.1 200 OK 74 | X-DNS-Prefetch-Control: off 75 | X-Frame-Options: SAMEORIGIN 76 | Strict-Transport-Security: max-age=15552000; includeSubDomains 77 | X-Download-Options: noopen 78 | X-Content-Type-Options: nosniff 79 | X-XSS-Protection: 1; mode=block 80 | Content-Type: text/html; charset=utf-8 81 | Content-Length: 592 82 | ETag: W/"250-IiCMcM1ZUFSswSYCU0KeFYFEMO8" 83 | Date: Sat, 25 Aug 2018 15:45:46 GMT 84 | Connection: keep-alive 85 | ``` 86 | 87 | ## 浏览器登录 88 | 89 | > 默认用户名:admin 密码:123456 记得要修改默认密码哦 90 | > 如果登录连续输错密码超过一定次数,会限定无法再登录. 需要清空 redis 缓存 91 | 92 | ```shell 93 | $ docker exec -it code-push-server_redis_1 redis-cli # 进入redis 94 | > flushall 95 | > quit 96 | ``` 97 | 98 | ## 查看服务日志 99 | 100 | ```shell 101 | $ docker-compose logs server 102 | ``` 103 | 104 | ## 查看存储 `docker volume ls` 105 | 106 | | DRIVER | VOLUME NAME | 描述 | 107 | | ------ | ----------------------------- | ------------------------------ | 108 | | local | code-push-server_data-mysql | 数据库存储数据目录 | 109 | | local | code-push-server_data-storage | 存储打包文件目录 | 110 | | local | code-push-server_data-tmp | 用于计算更新包差异文件临时目录 | 111 | | local | code-push-server_data-redis | redis 落地数据 | 112 | 113 | ## 销毁退出应用 114 | 115 | ```shell 116 | $ docker-compose down 117 | ``` 118 | -------------------------------------------------------------------------------- /docs/install-server-by-docker.md: -------------------------------------------------------------------------------- 1 | # docker deploy code-push-server 2 | 3 | [[Chinese version 中文版]](./install-server-by-docker.cn.md) 4 | 5 | > This document is used to describe docker deployment code-push-server, the example consists of three parts 6 | 7 | - code-push-server section 8 | - Update packages are stored in `local` by default (i.e. stored on the local machine). Using the docker volume storage method, container destruction will not cause data loss unless the volume is manually deleted. 9 | - The pm2 cluster mode is used to manage processes internally. The default number of open processes is the number of cpus. You can set the deploy parameter in the docker-compose.yml file according to your own machine configuration. 10 | - docker-compose.yml only provides some parameter settings of the application. If you need to set other configurations, you can modify the file config.js. 11 | - mysql section 12 | - Data is stored using docker volume, and container destruction will not cause data loss unless the volume is manually deleted. 13 | - Do not use the root user for the application. For security, you can create permissions with relatively small permissions for use by code-push-server. You only need to give `select, update, insert` permissions. To initialize the database, you need to use root or a user with table building privileges 14 | - redis part 15 | - `tryLoginTimes` login error limit 16 | - `updateCheckCache` improves application performance 17 | - `rolloutClientUniqueIdCache` grayscale release 18 | 19 | ## Install Docker 20 | 21 | Refer to the official Docker installation tutorial 22 | 23 | - [>>mac click here](https://docs.docker.com/docker-for-mac/install/) 24 | - [>>windows click here](https://docs.docker.com/docker-for-windows/install/) 25 | - [>>linux click here](https://docs.docker.com/install/linux/docker-ce/ubuntu/) 26 | 27 | `$ docker info` can successfully output relevant information, the installation is successful, and the following steps can be continued 28 | 29 | ## get code 30 | 31 | ```shell 32 | $ git clone https://github.com/shm-open/code-push-server.git 33 | $ cd code-push-server 34 | ```` 35 | 36 | ## Modify the configuration file 37 | 38 | ```shell 39 | $ vim docker-compose.yml 40 | ```` 41 | 42 | _Replace `YOUR_MACHINE_IP` in `DOWNLOAD_URL` with your own external network ip or domain name_ 43 | 44 | ### jwt.tokenSecret modification 45 | 46 | > code-push-server verifies the json web token encryption method used by the login authentication method. The symmetric encryption algorithm is public, so it is very important to modify the tokenSecret value in config.js. 47 | 48 | _Very important! Very important! Very important! _ 49 | 50 | > You can open the connection `https://www.grc.com/passwords.htm` to obtain a randomly generated number of type `63 random alpha-numeric characters` as the key 51 | 52 | _Replace `YOUR_JWT_TOKEN_SECRET` in `TOKEN_SECRET` with the key_ 53 | 54 | ## deploy 55 | 56 | ```shell 57 | $ docker-compose up -d 58 | ```` 59 | 60 | > If the internet speed is not good, a long and patient wait is required. . . Let's chat with the girl for a while ^\_^ 61 | 62 | ## View progress 63 | 64 | ```shell 65 | $ docker-compose ps 66 | ```` 67 | 68 | ## Access interface simple verification 69 | 70 | `$ curl -I http://YOUR_CODE_PUSH_SERVER_IP:3000/` 71 | 72 | returns `200 OK` 73 | 74 | ````http 75 | HTTP/1.1 200 OK 76 | X-DNS-Prefetch-Control: off 77 | X-Frame-Options: SAMEORIGIN 78 | Strict-Transport-Security: max-age=15552000; includeSubDomains 79 | X-Download-Options: noopen 80 | X-Content-Type-Options: nosniff 81 | X-XSS-Protection: 1; mode=block 82 | Content-Type: text/html; charset=utf-8 83 | Content-Length: 592 84 | ETag: W/"250-IiCMcM1ZUFSswSYCU0KeFYFEMO8" 85 | Date: Sat, 25 Aug 2018 15:45:46 GMT 86 | Connection: keep-alive 87 | ```` 88 | 89 | ## Browser login 90 | 91 | > Default username: admin Password: 123456 Remember to change the default password 92 | > If you log in and enter the wrong password for more than a certain number of times, you will no longer be able to log in. You need to clear the redis cache 93 | 94 | ```shell 95 | $ docker exec -it code-push-server_redis_1 redis-cli # Enter redis 96 | > flushall 97 | > quit 98 | ```` 99 | 100 | ## View service log 101 | 102 | ```shell 103 | $ docker-compose logs server 104 | ```` 105 | 106 | ## View storage `docker volume ls` 107 | 108 | | DRIVER | VOLUME NAME | DESCRIPTION | 109 | | ------ | ----------------------------- | ------------ ------------------ | 110 | | local | code-push-server_data-mysql | database storage data directory | 111 | | local | code-push-server_data-storage | Storage package file directory | 112 | | local | code-push-server_data-tmp | Temporary directory for calculating update package difference files | 113 | | local | code-push-server_data-redis | redis landing data | 114 | 115 | ## destroy exit application 116 | 117 | ```shell 118 | $ docker-compose down 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/install-server.md: -------------------------------------------------------------------------------- 1 | ## Install Node and NPM 2 | 3 | [Node Downloads](https://nodejs.org/en/download/) 4 | 5 | > (choose latest LTS version) 6 | 7 | ## Install PM2 8 | 9 | ```bash 10 | $ sudo npm i -g pm2 11 | ``` 12 | 13 | ## Install MySQL 14 | 15 | - [Linux](https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html) 16 | - [macOS](https://dev.mysql.com/doc/refman/8.0/en/osx-installation.html) 17 | - [Microsoft Windows](https://dev.mysql.com/doc/refman/8.0/en/windows-installation.html) 18 | - [Others](https://dev.mysql.com/doc/refman/8.0/en/installing.html) 19 | 20 | > notice. mysql8.x default auth caching_sha2_pasword not support in node-mysql2 see [issue](https://github.com/mysqljs/mysql/pull/1962) 21 | 22 | ## Install Redis 23 | 24 | - [Redis Quick Start](https://redis.io/topics/quickstart) 25 | 26 | ## Get code-push-server from NPM 27 | 28 | ```shell 29 | $ git clone https://github.com/byronigoe/code-push-server.git 30 | $ cd code-push-server 31 | $ npm install 32 | ``` 33 | 34 | # GET Redis 35 | 36 | ```shell 37 | yum install redis.x86_64 38 | ``` 39 | [Redis for Windows](https://github.com/microsoftarchive/redis/releases) 40 | 41 | ## INIT DATABASE 42 | 43 | Create a MySQL user, e.g. 44 | ```shell 45 | CREATE USER 'codepush'@'localhost' IDENTIFIED BY 'create_a_password'; 46 | ``` 47 | 48 | Grant appropriate permissions, e.g. 49 | ```shell 50 | GRANT ALL PRIVILEGES ON codepush . * TO 'codepush'@'localhost'; 51 | ``` 52 | 53 | Full command 54 | ```shell 55 | $ code-push-server-db init --dbhost "your mysql host" --dbport "your mysql port" --dbname "your database" --dbuser "your mysql user" --dbpassword "your mysql password" 56 | ``` 57 | 58 | Defaults (if omitted) are: 59 | | dbhost | localhost | 60 | | dbport | 3306 | 61 | | dbname | codepush | 62 | | dbuser | root | 63 | 64 | Minimally 65 | ```shell 66 | $ code-push-server-db init --dbpassword "your mysql root password" 67 | ``` 68 | 69 | or from source code 70 | 71 | ```shell 72 | $ ./bin/db init --dbhost "your mysql host" --dbport "your mysql port" --dbname "your database" --dbuser "your mysql user" --dbpassword "your mysql password" 73 | ``` 74 | 75 | > output: success 76 | 77 | ## Configure code-push-server 78 | 79 | check out the supported config items in [config.ts](../src/core/config.ts) 80 | 81 | Save the file [config.js](https://github.com/byronigoe/code-push-server/blob/master/config/config.js) and modify the properties, or set the corresponding environment variables (e.g. in process.json). 82 | 83 | - `local`.`storageDir` change to your directory, make sure you have read/write permissions. 84 | - `local`.`downloadUrl` replace `127.0.0.1` to your machine's IP. 85 | - `common`.`dataDir` change to your directory, make sure you have read/write permissions. 86 | - `jwt`.`tokenSecret` get a random string from `https://www.grc.com/passwords.htm`, and replace the value `INSERT_RANDOM_TOKEN_KEY`. 87 | - `db` config: `username`,`password`,`host`,`port` set the environment variables, or change them in this file. 88 | - `smtpConfig` config: `host`,`auth.user`,`auth.pass` needed if you enable `common.allowRegistration` 89 | 90 | ## CONFIGURE for pm2 91 | 92 | Save the file [process.json](https://github.com/byronigoe/code-push-server/blob/master/process.json) 93 | 94 | Some configuration properties have to change: 95 | 96 | - `script` if you install code-push-server from npm use `code-push-server`, or use `"your source code dir"/bin/www` 97 | - `CONFIG_FILE` absolute path to the config.js you downloaded. 98 | 99 | ## START SERVICE 100 | 101 | ```shell 102 | $ pm2 start process.json 103 | ``` 104 | 105 | ## Restart Service 106 | 107 | ```shell 108 | $ pm2 reload process.json 109 | ``` 110 | 111 | ## Stop Service 112 | 113 | ```shell 114 | $ pm2 stop process.json 115 | ``` 116 | 117 | ## Check Service is OK 118 | 119 | ```shell 120 | $ curl -I https://your-server.com/ 121 | ``` 122 | 123 | > return httpCode `200 OK` 124 | 125 | ```http 126 | HTTP/1.1 200 OK 127 | X-DNS-Prefetch-Control: off 128 | X-Frame-Options: SAMEORIGIN 129 | Strict-Transport-Security: max-age=15552000; includeSubDomains 130 | X-Download-Options: noopen 131 | X-Content-Type-Options: nosniff 132 | X-XSS-Protection: 1; mode=block 133 | Content-Type: text/html; charset=utf-8 134 | Content-Length: 592 135 | ETag: W/"250-IiCMcM1ZUFSswSYCU0KeFYFEMO8" 136 | Date: Sat, 25 Aug 2018 15:45:46 GMT 137 | Connection: keep-alive 138 | ``` 139 | 140 | ## Use Redis improve concurrency and security 141 | 142 | > config redis in config.js 143 | 144 | - `updateCheckCache` 145 | - `rolloutClientUniqueIdCache` 146 | - `tryLoginTimes` 147 | 148 | ## Upgrade from old version 149 | 150 | ```shell 151 | $ npm install -g @shm-open/code-push-server@latest 152 | $ code-push-server-db upgrade --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" # upgrade codepush database 153 | $ pm2 restart code-push-server # restart service 154 | ``` 155 | 156 | _from source code_ 157 | 158 | ```shell 159 | $ cd /path/to/code-push-server 160 | $ git pull --rebase origin master 161 | $ ./bin/db upgrade --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" 162 | # upgrade codepush database 163 | $ pm2 restart code-push-server # restart service 164 | ``` 165 | 166 | ## View pm2 logs 167 | 168 | ```shell 169 | $ pm2 ls 170 | $ pm2 show code-push-server 171 | $ tail -f "output file path" 172 | ``` 173 | 174 | ## Support Storage mode 175 | 176 | - local (default) 177 | - s3 (aws) 178 | - qiniu (qiniu) 179 | - oss (aliyun) 180 | - tencentcloud 181 | 182 | ## Default listen Host/Port 0.0.0.0/3000 183 | 184 | > you can change it in process.json, env: PORT,HOST 185 | 186 | ## [code-push-cli](https://github.com/byronigoe/code-push-cli) 187 | 188 | > Use code-push-cli manage CodePush Server 189 | 190 | ```shell 191 | $ npm install https://github.com/byronigoe/code-push-cli@latest -g 192 | $ code-push register https://your-server.com #or login with default account:admin password:123456 193 | ``` 194 | 195 | ## Configure a react-native project 196 | 197 | > Follow the react-native-code-push docs, addition iOS add a new entry named CodePushServerURL, whose value is the key of ourself CodePushServer URL. Android use the new CodePush constructor in MainApplication point CodePushServerUrl 198 | 199 | iOS eg. in file Info.plist 200 | 201 | ```xml 202 | ... 203 | CodePushDeploymentKey 204 | YourCodePushKey 205 | CodePushServerURL 206 | YourCodePushServerUrl 207 | ... 208 | ``` 209 | 210 | Android eg. in file MainApplication.java 211 | 212 | ```java 213 | @Override 214 | protected List getPackages() { 215 | return Arrays.asList( 216 | new MainReactPackage(), 217 | new CodePush( 218 | "YourKey", 219 | MainApplication.this, 220 | BuildConfig.DEBUG, 221 | "YourCodePushServerUrl" 222 | ) 223 | ); 224 | } 225 | ``` 226 | 227 | ## [cordova-plugin-code-push](https://github.com/Microsoft/cordova-plugin-code-push) for cordova 228 | 229 | ```shell 230 | $ cd /path/to/project 231 | $ cordova plugin add cordova-plugin-code-push@latest --save 232 | ``` 233 | 234 | ## Configure a Cordova project 235 | 236 | edit config.xml. add code below. 237 | 238 | ```xml 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | ``` 247 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Please sign in": "Please sign in", 3 | "email address": "email address", 4 | "username": "username", 5 | "password": "password", 6 | "Remember me": "Remember me", 7 | "Log in": "Log in", 8 | "hot update server": "hot update server", 9 | "Change Password": "Change Password", 10 | "Obtain": "Obtain", 11 | "old password": "old password", 12 | "new password": "new password", 13 | "please login again": "please login again", 14 | "change success": "change success", 15 | "Logout": "Logout", 16 | "Register": "Register" 17 | } -------------------------------------------------------------------------------- /locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Please sign in": "请登录", 3 | "email address": "邮箱地址", 4 | "username": "用户名", 5 | "password": "密码", 6 | "Remember me": "记住我", 7 | "Log in": "登录", 8 | "hot update server": "热更新服务器", 9 | "Change Password": "修改密码", 10 | "Obtain": "获取", 11 | "old password": "原密码", 12 | "new password": "新密码", 13 | "please login again": "请重新登录", 14 | "change success": "修改成功", 15 | "Logout": "登出" 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@byronigoe/code-push-server", 3 | "description": "Code Push Server is a self-hosted hot update service for react-native-code-push and cordova-plugin-code-push", 4 | "version": "2.1.2", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/byronigoe/code-push-server.git" 9 | }, 10 | "keywords": [ 11 | "code-push", 12 | "react-native", 13 | "cordova", 14 | "services", 15 | "code", 16 | "push" 17 | ], 18 | "author": "byronigoe", 19 | "bin": { 20 | "code-push-server": "./bin/www.js", 21 | "code-push-server-db": "./bin/db.js" 22 | }, 23 | "engines": { 24 | "node": ">= 6.0", 25 | "npm": ">= 3.10.8" 26 | }, 27 | "scripts": { 28 | "dev": "npm run build && concurrently npm:dev:tsc npm:dev:run", 29 | "dev:tsc": "tsc --watch", 30 | "dev:run": "LOG_LEVEL=debug supervisor -e 'node|js|ts|pug|json' ./bin/www.js", 31 | "start": "node ./bin/www.js", 32 | "init": "node ./bin/db.js init", 33 | "upgrade": "node ./bin/db.js upgrade", 34 | "build": "rm -rf bin && tsc", 35 | "release": "npm run build && npm test && standard-version && git push --follow-tags origin master && npm publish", 36 | "release-docker": "make release-docker", 37 | "test": "make test", 38 | "coverage": "make coverage", 39 | "lint": "eslint src" 40 | }, 41 | "dependencies": { 42 | "aliyun-oss-upload-stream": "1.3.0", 43 | "aliyun-sdk": "1.12.4", 44 | "aws-sdk": "2.1100.0", 45 | "bcryptjs": "2.4.3", 46 | "body-parser": "1.19.2", 47 | "cookie-parser": "1.4.6", 48 | "cos-nodejs-sdk-v5": "2.11.6", 49 | "express": "4.17.3", 50 | "extract-zip": "2.0.1", 51 | "formidable": "2.0.1", 52 | "fs-extra": "10.0.1", 53 | "helmet": "5.0.2", 54 | "i18n": "0.14.2", 55 | "jsonwebtoken": "8.5.1", 56 | "kv-logger": "0.5.3", 57 | "lodash": "4.17.21", 58 | "moment": "2.29.1", 59 | "mysql2": "2.3.3", 60 | "node-fetch": "2.6.7", 61 | "nodemailer": "6.7.3", 62 | "pug": "3.0.2", 63 | "qiniu": "7.4.0", 64 | "rand-token": "1.0.1", 65 | "recursive-readdir": "2.2.2", 66 | "redis": "4.0.4", 67 | "sequelize": "6.17.0", 68 | "slash": "3.0.0", 69 | "validator": "13.7.0", 70 | "yargs": "17.4.0", 71 | "yazl": "2.5.1" 72 | }, 73 | "devDependencies": { 74 | "@shm-open/eslint-config-bundle": "1.9.7", 75 | "@types/bcryptjs": "2.4.2", 76 | "@types/body-parser": "1.19.2", 77 | "@types/cookie-parser": "1.4.2", 78 | "@types/formidable": "2.0.4", 79 | "@types/fs-extra": "9.0.13", 80 | "@types/i18n": "0.13.2", 81 | "@types/jsonwebtoken": "8.5.8", 82 | "@types/lodash": "4.14.181", 83 | "@types/node-fetch": "2.6.1", 84 | "@types/nodemailer": "6.4.4", 85 | "@types/recursive-readdir": "2.2.0", 86 | "@types/validator": "13.7.2", 87 | "@types/yazl": "2.4.2", 88 | "concurrently": "7.0.0", 89 | "mocha": "9.2.2", 90 | "nyc": "15.1.0", 91 | "should": "13.2.3", 92 | "standard-version": "9.3.2", 93 | "supertest": "6.2.2", 94 | "supervisor": "0.12.0", 95 | "typescript": "4.6.3" 96 | }, 97 | "files": [ 98 | "bin", 99 | "docs", 100 | "locales", 101 | "public", 102 | "sql", 103 | "views", 104 | "README.md", 105 | "LICENSE" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /process.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "code-push-server", 5 | "max_memory_restart": "500M", 6 | "script": "code-push-server", 7 | "instances": "max", // Number of open instances, 'max' is the number of cpu cores 8 | "exec_mode": "cluster", // Cluster mode to maximize website concurrency 9 | "env": { 10 | "NODE_ENV": "production", 11 | "PORT": 3000, 12 | "CONFIG_FILE": "/path/to/config.js" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/public/js/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | .site-notice { 2 | padding: 5px 0; 3 | text-align: center; 4 | background-color: #fff; 5 | } 6 | -------------------------------------------------------------------------------- /public/stylesheets/signin.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee; 5 | } 6 | 7 | .form-signin { 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: 0 auto; 11 | } 12 | .form-signin .form-signin-heading, 13 | .form-signin .checkbox { 14 | margin-bottom: 10px; 15 | } 16 | .form-signin .checkbox { 17 | font-weight: normal; 18 | } 19 | .form-signin .form-control { 20 | position: relative; 21 | height: auto; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | padding: 10px; 26 | font-size: 16px; 27 | } 28 | .form-signin .form-control:focus { 29 | z-index: 2; 30 | } 31 | .form-signin input[type="email"] { 32 | margin-bottom: -1px; 33 | border-bottom-right-radius: 0; 34 | border-bottom-left-radius: 0; 35 | } 36 | .form-signin input[type="password"] { 37 | margin-bottom: 10px; 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } 41 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "devDependencies": { 4 | "automerge": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /routes/accessKeys.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var _ = require('lodash'); 4 | var security = require('../core/utils/security'); 5 | var models = require('../models'); 6 | var middleware = require('../core/middleware'); 7 | var accountManager = require('../core/services/account-manager')(); 8 | var AppError = require('../core/app-error'); 9 | var log4js = require('log4js'); 10 | var log = log4js.getLogger('cps:accessKey'); 11 | 12 | router.get('/', middleware.checkToken, (req, res, next) => { 13 | log.debug('request get acceesKeys'); 14 | var uid = req.users.id; 15 | accountManager 16 | .getAllAccessKeyByUid(uid) 17 | .then((accessKeys) => { 18 | log.debug('acceesKeys:', accessKeys); 19 | res.send({ accessKeys: accessKeys }); 20 | }) 21 | .catch((e) => { 22 | next(e); 23 | }); 24 | }); 25 | 26 | router.post('/', middleware.checkToken, (req, res, next) => { 27 | var uid = req.users.id; 28 | var identical = req.users.identical; 29 | var createdBy = _.trim(req.body.createdBy); 30 | var friendlyName = _.trim(req.body.friendlyName); 31 | var ttl = parseInt(req.body.ttl); 32 | var description = _.trim(req.body.description); 33 | log.debug(req.body); 34 | var newAccessKey = security.randToken(28).concat(identical); 35 | return accountManager 36 | .isExistAccessKeyName(uid, friendlyName) 37 | .then((data) => { 38 | if (!_.isEmpty(data)) { 39 | throw new AppError.AppError(`The access key "${friendlyName}" already exists.`); 40 | } 41 | }) 42 | .then(() => { 43 | return accountManager.createAccessKey( 44 | uid, 45 | newAccessKey, 46 | ttl, 47 | friendlyName, 48 | createdBy, 49 | description, 50 | ); 51 | }) 52 | .then((newToken) => { 53 | var moment = require('moment'); 54 | var info = { 55 | name: newToken.tokens, 56 | createdTime: parseInt(moment(newToken.created_at).format('x')), 57 | createdBy: newToken.created_by, 58 | expires: parseInt(moment(newToken.expires_at).format('x')), 59 | description: newToken.description, 60 | friendlyName: newToken.name, 61 | }; 62 | log.debug(info); 63 | res.send({ accessKey: info }); 64 | }) 65 | .catch((e) => { 66 | if (e instanceof AppError.AppError) { 67 | log.debug(e); 68 | res.status(406).send(e.message); 69 | } else { 70 | next(e); 71 | } 72 | }); 73 | }); 74 | 75 | router.delete('/:name', middleware.checkToken, (req, res, next) => { 76 | var name = _.trim(decodeURI(req.params.name)); 77 | var uid = req.users.id; 78 | return models.UserTokens.destroy({ where: { name: name, uid: uid } }) 79 | .then((rowNum) => { 80 | log.debug('delete acceesKey:', name); 81 | res.send({ friendlyName: name }); 82 | }) 83 | .catch((e) => { 84 | if (e instanceof AppError.AppError) { 85 | log.debug(e); 86 | res.status(406).send(e.message); 87 | } else { 88 | next(e); 89 | } 90 | }); 91 | }); 92 | module.exports = router; 93 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var AppError = require('../core/app-error'); 4 | var middleware = require('../core/middleware'); 5 | var ClientManager = require('../core/services/client-manager'); 6 | var _ = require('lodash'); 7 | var log4js = require('log4js'); 8 | var log = log4js.getLogger('cps:index'); 9 | 10 | router.get('/', (req, res, next) => { 11 | res.render('index', { title: 'CodePushServer' }); 12 | }); 13 | 14 | router.get('/tokens', (req, res) => { 15 | res.render('tokens', { title: 'Obtain token' }); 16 | }); 17 | 18 | router.get('/updateCheck', (req, res, next) => { 19 | var deploymentKey = _.get(req, 'query.deploymentKey'); 20 | var appVersion = _.get(req, 'query.appVersion'); 21 | var label = _.get(req, 'query.label'); 22 | var packageHash = _.get(req, 'query.packageHash'); 23 | var clientUniqueId = _.get(req, 'query.clientUniqueId'); 24 | var clientManager = new ClientManager(); 25 | log.debug('req.query', req.query); 26 | clientManager 27 | .updateCheckFromCache(deploymentKey, appVersion, label, packageHash, clientUniqueId) 28 | .then((rs) => { 29 | return clientManager 30 | .chosenMan(rs.packageId, rs.rollout, clientUniqueId) 31 | .then((data) => { 32 | if (!data) { 33 | rs.isAvailable = false; 34 | return rs; 35 | } 36 | return rs; 37 | }); 38 | }) 39 | .then((rs) => { 40 | delete rs.packageId; 41 | delete rs.rollout; 42 | res.send({ updateInfo: rs }); 43 | }) 44 | .catch((e) => { 45 | if (e instanceof AppError.AppError) { 46 | res.status(404).send(e.message); 47 | } else { 48 | next(e); 49 | } 50 | }); 51 | }); 52 | 53 | router.post('/reportStatus/download', (req, res) => { 54 | log.debug('req.body', req.body); 55 | var clientUniqueId = _.get(req, 'body.clientUniqueId'); 56 | var label = _.get(req, 'body.label'); 57 | var deploymentKey = _.get(req, 'body.deploymentKey'); 58 | var clientManager = new ClientManager(); 59 | clientManager.reportStatusDownload(deploymentKey, label, clientUniqueId).catch((err) => { 60 | if (!err instanceof AppError.AppError) { 61 | console.error(err.stack); 62 | } 63 | }); 64 | res.send('OK'); 65 | }); 66 | 67 | router.post('/reportStatus/deploy', (req, res) => { 68 | log.debug('req.body', req.body); 69 | var clientUniqueId = _.get(req, 'body.clientUniqueId'); 70 | var label = _.get(req, 'body.label'); 71 | var deploymentKey = _.get(req, 'body.deploymentKey'); 72 | var clientManager = new ClientManager(); 73 | clientManager 74 | .reportStatusDeploy(deploymentKey, label, clientUniqueId, req.body) 75 | .catch((err) => { 76 | if (!err instanceof AppError.AppError) { 77 | console.error(err.stack); 78 | } 79 | }); 80 | res.send('OK'); 81 | }); 82 | 83 | router.get('/authenticated', middleware.checkToken, (req, res) => { 84 | return res.send({ authenticated: true }); 85 | }); 86 | 87 | module.exports = router; 88 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var _ = require('lodash'); 4 | var models = require('../models'); 5 | var middleware = require('../core/middleware'); 6 | var AccountManager = require('../core/services/account-manager'); 7 | var AppError = require('../core/app-error'); 8 | 9 | router.get('/', middleware.checkToken, (req, res) => { 10 | res.send({ title: 'CodePushServer' }); 11 | }); 12 | 13 | router.post('/', (req, res, next) => { 14 | var email = _.trim(_.get(req, 'body.email')); 15 | var token = _.trim(_.get(req, 'body.token')); 16 | var password = _.trim(_.get(req, 'body.password')); 17 | var accountManager = new AccountManager(); 18 | return accountManager 19 | .checkRegisterCode(email, token) 20 | .then((u) => { 21 | if (_.isString(password) && password.length < 6) { 22 | throw new AppError.AppError('Please enter a password with a length of 6-20 digits'); 23 | } 24 | return accountManager.register(email, password); 25 | }) 26 | .then(() => { 27 | res.send({ status: 'OK' }); 28 | }) 29 | .catch((e) => { 30 | if (e instanceof AppError.AppError) { 31 | res.send({ status: 'ERROR', message: e.message }); 32 | } else { 33 | next(e); 34 | } 35 | }); 36 | }); 37 | 38 | router.get('/exists', (req, res, next) => { 39 | var email = _.trim(_.get(req, 'query.email')); 40 | models.Users.findOne({ where: { email: email } }) 41 | .then((u) => { 42 | if (!email) { 43 | throw new AppError.AppError(`Please enter your email address`); 44 | } 45 | res.send({ status: 'OK', exists: u ? true : false }); 46 | }) 47 | .catch((e) => { 48 | if (e instanceof AppError.AppError) { 49 | res.send({ status: 'ERROR', message: e.message }); 50 | } else { 51 | next(e); 52 | } 53 | }); 54 | }); 55 | 56 | router.post('/registerCode', (req, res, next) => { 57 | var email = _.get(req, 'body.email'); 58 | var accountManager = new AccountManager(); 59 | return accountManager 60 | .sendRegisterCode(email) 61 | .then(() => { 62 | res.send({ status: 'OK' }); 63 | }) 64 | .catch((e) => { 65 | if (e instanceof AppError.AppError) { 66 | res.send({ status: 'ERROR', message: e.message }); 67 | } else { 68 | next(e); 69 | } 70 | }); 71 | }); 72 | 73 | router.get('/registerCode/exists', (req, res, next) => { 74 | var email = _.trim(_.get(req, 'query.email')); 75 | var token = _.trim(_.get(req, 'query.token')); 76 | var accountManager = new AccountManager(); 77 | return accountManager 78 | .checkRegisterCode(email, token) 79 | .then(() => { 80 | res.send({ status: 'OK' }); 81 | }) 82 | .catch((e) => { 83 | if (e instanceof AppError.AppError) { 84 | res.send({ status: 'ERROR', message: e.message }); 85 | } else { 86 | next(e); 87 | } 88 | }); 89 | }); 90 | 91 | // Change password 92 | router.patch('/password', middleware.checkToken, (req, res, next) => { 93 | var oldPassword = _.trim(_.get(req, 'body.oldPassword')); 94 | var newPassword = _.trim(_.get(req, 'body.newPassword')); 95 | var uid = req.users.id; 96 | var accountManager = new AccountManager(); 97 | return accountManager 98 | .changePassword(uid, oldPassword, newPassword) 99 | .then(() => { 100 | res.send({ status: 'OK' }); 101 | }) 102 | .catch((e) => { 103 | if (e instanceof AppError.AppError) { 104 | res.send({ status: 'ERROR', message: e.message }); 105 | } else { 106 | next(e); 107 | } 108 | }); 109 | }); 110 | 111 | module.exports = router; 112 | -------------------------------------------------------------------------------- /sql/codepush-all-docker.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `codepush`; 2 | 3 | CREATE USER IF NOT EXISTS 'codepush'@'%' IDENTIFIED BY '123456'; 4 | GRANT SELECT,UPDATE,INSERT ON `codepush`.* TO 'codepush'@'%'; 5 | flush privileges; 6 | 7 | use `codepush`; 8 | CREATE TABLE IF NOT EXISTS `apps` ( 9 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 10 | `name` varchar(50) NOT NULL DEFAULT '', 11 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 12 | `os` tinyint(3) unsigned NOT NULL DEFAULT '0', 13 | `platform` tinyint(3) unsigned NOT NULL DEFAULT '0', 14 | `is_use_diff_text` tinyint(3) unsigned NOT NULL DEFAULT '0', 15 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 16 | `created_at` timestamp NULL DEFAULT NULL, 17 | `deleted_at` timestamp NULL DEFAULT NULL, 18 | PRIMARY KEY (`id`), 19 | KEY `idx_name` (`name`(12)) 20 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 21 | 22 | CREATE TABLE IF NOT EXISTS `collaborators` ( 23 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 24 | `appid` int(10) unsigned NOT NULL DEFAULT '0', 25 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 26 | `roles` varchar(20) NOT NULL DEFAULT '', 27 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 28 | `created_at` timestamp NULL DEFAULT NULL, 29 | `deleted_at` timestamp NULL DEFAULT NULL, 30 | PRIMARY KEY (`id`), 31 | KEY `idx_appid` (`appid`), 32 | KEY `idx_uid` (`uid`) 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 34 | 35 | 36 | CREATE TABLE IF NOT EXISTS `deployments` ( 37 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 38 | `appid` int(10) unsigned NOT NULL DEFAULT '0', 39 | `name` varchar(20) NOT NULL DEFAULT '', 40 | `description` varchar(500) NOT NULL DEFAULT '', 41 | `deployment_key` varchar(64) NOT NULL, 42 | `last_deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', 43 | `label_id` int(11) unsigned NOT NULL DEFAULT '0', 44 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 45 | `created_at` timestamp NULL DEFAULT NULL, 46 | `deleted_at` timestamp NULL DEFAULT NULL, 47 | PRIMARY KEY (`id`), 48 | KEY `idx_appid` (`appid`), 49 | KEY `idx_deploymentkey` (`deployment_key`(40)) 50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 51 | 52 | CREATE TABLE IF NOT EXISTS `deployments_history` ( 53 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 54 | `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', 55 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 56 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 57 | `deleted_at` timestamp NULL DEFAULT NULL, 58 | PRIMARY KEY (`id`), 59 | KEY `idx_deployment_id` (`deployment_id`) 60 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 61 | 62 | 63 | CREATE TABLE IF NOT EXISTS `deployments_versions` ( 64 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 65 | `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', 66 | `app_version` varchar(100) NOT NULL DEFAULT '', 67 | `current_package_id` int(10) unsigned NOT NULL DEFAULT '0', 68 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 69 | `created_at` timestamp NULL DEFAULT NULL, 70 | `deleted_at` timestamp NULL DEFAULT NULL, 71 | `min_version` bigint(20) unsigned NOT NULL DEFAULT '0', 72 | `max_version` bigint(20) unsigned NOT NULL DEFAULT '0', 73 | PRIMARY KEY (`id`), 74 | KEY `idx_did_minversion` (`deployment_id`,`min_version`), 75 | KEY `idx_did_maxversion` (`deployment_id`,`max_version`), 76 | KEY `idx_did_appversion` (`deployment_id`,`app_version`(30)) 77 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 78 | 79 | CREATE TABLE IF NOT EXISTS `packages` ( 80 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 81 | `deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', 82 | `deployment_id` int(10) unsigned NOT NULL DEFAULT '0', 83 | `description` varchar(500) NOT NULL DEFAULT '', 84 | `package_hash` varchar(64) NOT NULL DEFAULT '', 85 | `blob_url` varchar(255) NOT NULL DEFAULT '', 86 | `size` int(11) unsigned NOT NULL DEFAULT '0', 87 | `manifest_blob_url` varchar(255) NOT NULL DEFAULT '', 88 | `release_method` varchar(20) NOT NULL DEFAULT '', 89 | `label` varchar(20) NOT NULL DEFAULT '', 90 | `original_label` varchar(20) NOT NULL DEFAULT '', 91 | `original_deployment` varchar(20) NOT NULL DEFAULT '', 92 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 93 | `created_at` timestamp NULL DEFAULT NULL, 94 | `released_by` bigint(20) unsigned NOT NULL DEFAULT '0', 95 | `is_mandatory` tinyint(3) unsigned NOT NULL DEFAULT '0', 96 | `is_disabled` tinyint(3) unsigned NOT NULL DEFAULT '0', 97 | `rollout` tinyint(3) unsigned NOT NULL DEFAULT '0', 98 | `deleted_at` timestamp NULL DEFAULT NULL, 99 | PRIMARY KEY (`id`), 100 | KEY `idx_deploymentid_label` (`deployment_id`,`label`(8)), 101 | KEY `idx_versions_id` (`deployment_version_id`) 102 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 103 | 104 | CREATE TABLE IF NOT EXISTS `packages_diff` ( 105 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 106 | `package_id` int(11) unsigned NOT NULL DEFAULT '0', 107 | `diff_against_package_hash` varchar(64) NOT NULL DEFAULT '', 108 | `diff_blob_url` varchar(255) NOT NULL DEFAULT '', 109 | `diff_size` int(11) unsigned NOT NULL DEFAULT '0', 110 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 111 | `created_at` timestamp NULL DEFAULT NULL, 112 | `deleted_at` timestamp NULL DEFAULT NULL, 113 | PRIMARY KEY (`id`), 114 | KEY `idx_packageid_hash` (`package_id`,`diff_against_package_hash`(40)) 115 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 116 | 117 | CREATE TABLE IF NOT EXISTS `packages_metrics` ( 118 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 119 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 120 | `active` int(10) unsigned NOT NULL DEFAULT '0', 121 | `downloaded` int(10) unsigned NOT NULL DEFAULT '0', 122 | `failed` int(10) unsigned NOT NULL DEFAULT '0', 123 | `installed` int(10) unsigned NOT NULL DEFAULT '0', 124 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 125 | `created_at` timestamp NULL DEFAULT NULL, 126 | `deleted_at` timestamp NULL DEFAULT NULL, 127 | PRIMARY KEY (`id`), 128 | KEY `idx_packageid` (`package_id`) 129 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 130 | 131 | CREATE TABLE IF NOT EXISTS `user_tokens` ( 132 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 133 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 134 | `name` varchar(50) NOT NULL DEFAULT '', 135 | `tokens` varchar(64) NOT NULL DEFAULT '', 136 | `created_by` varchar(64) NOT NULL DEFAULT '', 137 | `description` varchar(500) NOT NULL DEFAULT '', 138 | `is_session` tinyint(3) unsigned NOT NULL DEFAULT '0', 139 | `expires_at` timestamp NULL DEFAULT NULL, 140 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 141 | `deleted_at` timestamp NULL DEFAULT NULL, 142 | PRIMARY KEY (`id`), 143 | KEY `idx_uid` (`uid`), 144 | KEY `idx_tokens` (`tokens`) KEY_BLOCK_SIZE=16 145 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 146 | 147 | CREATE TABLE IF NOT EXISTS `users` ( 148 | `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, 149 | `username` varchar(50) NOT NULL DEFAULT '', 150 | `password` varchar(255) NOT NULL DEFAULT '', 151 | `email` varchar(100) NOT NULL DEFAULT '', 152 | `identical` varchar(10) NOT NULL DEFAULT '', 153 | `ack_code` varchar(10) NOT NULL DEFAULT '', 154 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 155 | `created_at` timestamp NULL DEFAULT NULL, 156 | PRIMARY KEY (`id`), 157 | UNIQUE KEY `udx_identical` (`identical`), 158 | KEY `udx_username` (`username`), 159 | KEY `idx_email` (`email`) KEY_BLOCK_SIZE=20 160 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 161 | 162 | INSERT INTO `users` (`id`, `username`, `password`, `email`, `identical`, `ack_code`, `updated_at`, `created_at`) 163 | VALUES 164 | (1,'admin','$2a$12$mvUY9kTqW4kSoGuZFDW0sOSgKmNY8SPHVyVrSckBTLtXKf6vKX3W.','lisong2010@gmail.com','4ksvOXqog','oZmGE','2016-11-14 10:46:55','2016-02-29 21:24:49'); 165 | 166 | 167 | CREATE TABLE IF NOT EXISTS `versions` ( 168 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 169 | `type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1.DBversion', 170 | `version` varchar(10) NOT NULL DEFAULT '', 171 | PRIMARY KEY (`id`), 172 | UNIQUE KEY `udx_type` (`type`) 173 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 174 | 175 | LOCK TABLES `versions` WRITE; 176 | INSERT INTO `versions` (`id`, `type`, `version`) 177 | VALUES 178 | (1,1,'0.5.0'); 179 | UNLOCK TABLES; 180 | 181 | CREATE TABLE IF NOT EXISTS `log_report_deploy` ( 182 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 183 | `status` tinyint(3) unsigned NOT NULL DEFAULT '0', 184 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 185 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 186 | `previous_label` varchar(20) NOT NULL DEFAULT '', 187 | `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', 188 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 189 | PRIMARY KEY (`id`) 190 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 191 | 192 | CREATE TABLE IF NOT EXISTS `log_report_download` ( 193 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 194 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 195 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 196 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 197 | PRIMARY KEY (`id`) 198 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 199 | -------------------------------------------------------------------------------- /sql/codepush-all.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `apps` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `name` varchar(50) NOT NULL DEFAULT '', 4 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 5 | `os` tinyint(3) unsigned NOT NULL DEFAULT '0', 6 | `platform` tinyint(3) unsigned NOT NULL DEFAULT '0', 7 | `is_use_diff_text` tinyint(3) unsigned NOT NULL DEFAULT '0', 8 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 9 | `created_at` timestamp NULL DEFAULT NULL, 10 | `deleted_at` timestamp NULL DEFAULT NULL, 11 | PRIMARY KEY (`id`), 12 | KEY `idx_name` (`name`(12)) 13 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 14 | 15 | CREATE TABLE IF NOT EXISTS `collaborators` ( 16 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 17 | `appid` int(10) unsigned NOT NULL DEFAULT '0', 18 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 19 | `roles` varchar(20) NOT NULL DEFAULT '', 20 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 21 | `created_at` timestamp NULL DEFAULT NULL, 22 | `deleted_at` timestamp NULL DEFAULT NULL, 23 | PRIMARY KEY (`id`), 24 | KEY `idx_appid` (`appid`), 25 | KEY `idx_uid` (`uid`) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 27 | 28 | CREATE TABLE IF NOT EXISTS `deployments` ( 29 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 30 | `appid` int(10) unsigned NOT NULL DEFAULT '0', 31 | `name` varchar(20) NOT NULL DEFAULT '', 32 | `description` varchar(500) NOT NULL DEFAULT '', 33 | `deployment_key` varchar(64) NOT NULL, 34 | `last_deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', 35 | `label_id` int(11) unsigned NOT NULL DEFAULT '0', 36 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 37 | `created_at` timestamp NULL DEFAULT NULL, 38 | `deleted_at` timestamp NULL DEFAULT NULL, 39 | PRIMARY KEY (`id`), 40 | KEY `idx_appid` (`appid`), 41 | KEY `idx_deploymentkey` (`deployment_key`(40)) 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 43 | 44 | CREATE TABLE IF NOT EXISTS `deployments_history` ( 45 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 46 | `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', 47 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 48 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 49 | `deleted_at` timestamp NULL DEFAULT NULL, 50 | PRIMARY KEY (`id`), 51 | KEY `idx_deployment_id` (`deployment_id`) 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 53 | 54 | CREATE TABLE IF NOT EXISTS `deployments_versions` ( 55 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 56 | `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', 57 | `app_version` varchar(100) NOT NULL DEFAULT '', 58 | `current_package_id` int(10) unsigned NOT NULL DEFAULT '0', 59 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 60 | `created_at` timestamp NULL DEFAULT NULL, 61 | `deleted_at` timestamp NULL DEFAULT NULL, 62 | `min_version` bigint(20) unsigned NOT NULL DEFAULT '0', 63 | `max_version` bigint(20) unsigned NOT NULL DEFAULT '0', 64 | PRIMARY KEY (`id`), 65 | KEY `idx_did_minversion` (`deployment_id`,`min_version`), 66 | KEY `idx_did_maxversion` (`deployment_id`,`max_version`), 67 | KEY `idx_did_appversion` (`deployment_id`,`app_version`(30)) 68 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 69 | 70 | CREATE TABLE IF NOT EXISTS `packages` ( 71 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 72 | `deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', 73 | `deployment_id` int(10) unsigned NOT NULL DEFAULT '0', 74 | `description` varchar(500) NOT NULL DEFAULT '', 75 | `package_hash` varchar(64) NOT NULL DEFAULT '', 76 | `blob_url` varchar(255) NOT NULL DEFAULT '', 77 | `size` int(11) unsigned NOT NULL DEFAULT '0', 78 | `manifest_blob_url` varchar(255) NOT NULL DEFAULT '', 79 | `release_method` varchar(20) NOT NULL DEFAULT '', 80 | `label` varchar(20) NOT NULL DEFAULT '', 81 | `original_label` varchar(20) NOT NULL DEFAULT '', 82 | `original_deployment` varchar(20) NOT NULL DEFAULT '', 83 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 84 | `created_at` timestamp NULL DEFAULT NULL, 85 | `released_by` bigint(20) unsigned NOT NULL DEFAULT '0', 86 | `is_mandatory` tinyint(3) unsigned NOT NULL DEFAULT '0', 87 | `is_disabled` tinyint(3) unsigned NOT NULL DEFAULT '0', 88 | `rollout` tinyint(3) unsigned NOT NULL DEFAULT '0', 89 | `deleted_at` timestamp NULL DEFAULT NULL, 90 | PRIMARY KEY (`id`), 91 | KEY `idx_deploymentid_label` (`deployment_id`,`label`(8)), 92 | KEY `idx_versions_id` (`deployment_version_id`) 93 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 94 | 95 | CREATE TABLE IF NOT EXISTS `packages_diff` ( 96 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 97 | `package_id` int(11) unsigned NOT NULL DEFAULT '0', 98 | `diff_against_package_hash` varchar(64) NOT NULL DEFAULT '', 99 | `diff_blob_url` varchar(255) NOT NULL DEFAULT '', 100 | `diff_size` int(11) unsigned NOT NULL DEFAULT '0', 101 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 102 | `created_at` timestamp NULL DEFAULT NULL, 103 | `deleted_at` timestamp NULL DEFAULT NULL, 104 | PRIMARY KEY (`id`), 105 | KEY `idx_packageid_hash` (`package_id`,`diff_against_package_hash`(40)) 106 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 107 | 108 | CREATE TABLE IF NOT EXISTS `packages_metrics` ( 109 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 110 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 111 | `active` int(10) unsigned NOT NULL DEFAULT '0', 112 | `downloaded` int(10) unsigned NOT NULL DEFAULT '0', 113 | `failed` int(10) unsigned NOT NULL DEFAULT '0', 114 | `installed` int(10) unsigned NOT NULL DEFAULT '0', 115 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 116 | `created_at` timestamp NULL DEFAULT NULL, 117 | `deleted_at` timestamp NULL DEFAULT NULL, 118 | PRIMARY KEY (`id`), 119 | KEY `idx_packageid` (`package_id`) 120 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 121 | 122 | CREATE TABLE IF NOT EXISTS `user_tokens` ( 123 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 124 | `uid` bigint(20) unsigned NOT NULL DEFAULT '0', 125 | `name` varchar(50) NOT NULL DEFAULT '', 126 | `tokens` varchar(64) NOT NULL DEFAULT '', 127 | `created_by` varchar(64) NOT NULL DEFAULT '', 128 | `description` varchar(500) NOT NULL DEFAULT '', 129 | `is_session` tinyint(3) unsigned NOT NULL DEFAULT '0', 130 | `expires_at` timestamp NULL DEFAULT NULL, 131 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 132 | `deleted_at` timestamp NULL DEFAULT NULL, 133 | PRIMARY KEY (`id`), 134 | KEY `idx_uid` (`uid`), 135 | KEY `idx_tokens` (`tokens`) KEY_BLOCK_SIZE=16 136 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 137 | 138 | CREATE TABLE IF NOT EXISTS `users` ( 139 | `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, 140 | `username` varchar(50) NOT NULL DEFAULT '', 141 | `password` varchar(255) NOT NULL DEFAULT '', 142 | `email` varchar(100) NOT NULL DEFAULT '', 143 | `identical` varchar(10) NOT NULL DEFAULT '', 144 | `ack_code` varchar(10) NOT NULL DEFAULT '', 145 | `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 146 | `created_at` timestamp NULL DEFAULT NULL, 147 | PRIMARY KEY (`id`), 148 | UNIQUE KEY `udx_identical` (`identical`), 149 | KEY `udx_username` (`username`), 150 | KEY `idx_email` (`email`) KEY_BLOCK_SIZE=20 151 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 152 | 153 | INSERT INTO `users` (`id`, `username`, `password`, `email`, `identical`, `ack_code`, `updated_at`, `created_at`) 154 | VALUES 155 | (1,'admin','$2a$12$mvUY9kTqW4kSoGuZFDW0sOSgKmNY8SPHVyVrSckBTLtXKf6vKX3W.','lisong2010@gmail.com','4ksvOXqog','oZmGE','2016-11-14 10:46:55','2016-02-29 21:24:49'); 156 | 157 | CREATE TABLE IF NOT EXISTS `versions` ( 158 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 159 | `type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1.DBversion', 160 | `version` varchar(10) NOT NULL DEFAULT '', 161 | PRIMARY KEY (`id`), 162 | UNIQUE KEY `udx_type` (`type`) 163 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 164 | 165 | LOCK TABLES `versions` WRITE; 166 | INSERT INTO `versions` (`id`, `type`, `version`) 167 | VALUES 168 | (1,1,'0.5.0'); 169 | UNLOCK TABLES; 170 | 171 | CREATE TABLE IF NOT EXISTS `log_report_deploy` ( 172 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 173 | `status` tinyint(3) unsigned NOT NULL DEFAULT '0', 174 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 175 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 176 | `previous_label` varchar(20) NOT NULL DEFAULT '', 177 | `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', 178 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 179 | PRIMARY KEY (`id`) 180 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 181 | 182 | CREATE TABLE IF NOT EXISTS `log_report_download` ( 183 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 184 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 185 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 186 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 187 | PRIMARY KEY (`id`) 188 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 189 | -------------------------------------------------------------------------------- /sql/codepush-v0.2.14-patch.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `versions`; 2 | CREATE TABLE `versions` ( 3 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 4 | `type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1.DBversion', 5 | `version` varchar(10) NOT NULL DEFAULT '', 6 | PRIMARY KEY (`id`), 7 | UNIQUE KEY `udx_type` (`type`) 8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 9 | 10 | LOCK TABLES `versions` WRITE; 11 | INSERT INTO `versions` (`id`, `type`, `version`) 12 | VALUES 13 | (1,1,'0.2.14'); 14 | UNLOCK TABLES; 15 | 16 | ALTER TABLE `packages` ADD `is_mandatory` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0'; 17 | -------------------------------------------------------------------------------- /sql/codepush-v0.2.15-patch.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE `apps` CHANGE `created_at` `created_at` TIMESTAMP NULL; 3 | ALTER TABLE `collaborators` CHANGE `created_at` `created_at` TIMESTAMP NULL; 4 | ALTER TABLE `deployments` CHANGE `created_at` `created_at` TIMESTAMP NULL; 5 | ALTER TABLE `deployments_history` CHANGE `created_at` `created_at` TIMESTAMP NULL; 6 | ALTER TABLE `deployments_versions` CHANGE `created_at` `created_at` TIMESTAMP NULL; 7 | ALTER TABLE `packages_diff` CHANGE `created_at` `created_at` TIMESTAMP NULL; 8 | ALTER TABLE `packages_metrics` CHANGE `created_at` `created_at` TIMESTAMP NULL; 9 | ALTER TABLE `user_tokens` CHANGE `expires_at` `expires_at` TIMESTAMP NULL,CHANGE `created_at` `created_at` TIMESTAMP NULL; 10 | ALTER TABLE `users` CHANGE `created_at` `created_at` TIMESTAMP NULL; 11 | 12 | DROP PROCEDURE IF EXISTS schema_change; 13 | CREATE PROCEDURE schema_change() 14 | BEGIN 15 | DECLARE CurrentDatabase VARCHAR(100); 16 | SELECT DATABASE() INTO CurrentDatabase; 17 | IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=CurrentDatabase AND table_name = 'packages' AND column_name = 'is_mandatory') THEN 18 | ALTER TABLE `packages` ADD `is_mandatory` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0'; 19 | END IF; 20 | END; 21 | CALL schema_change(); 22 | DROP PROCEDURE IF EXISTS schema_change; 23 | 24 | ALTER TABLE `deployments_versions` DROP INDEX idx_did_appversion, ADD INDEX `idx_did_appversion` (`deployment_id`, `app_version`),ADD `deleted_at` TIMESTAMP NULL; 25 | ALTER TABLE `packages` ADD `deleted_at` TIMESTAMP NULL; 26 | ALTER TABLE `packages_metrics` DROP INDEX `udx_packageid`,ADD INDEX `idx_packageid` (`package_id`),ADD `deleted_at` TIMESTAMP NULL; 27 | ALTER TABLE `packages_diff` ADD `deleted_at` TIMESTAMP NULL; 28 | UPDATE `versions` SET `version` = '0.2.15' WHERE `type` = '1'; 29 | -------------------------------------------------------------------------------- /sql/codepush-v0.3.0-patch.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `apps` ADD `os` TINYINT UNSIGNED NOT NULL DEFAULT 0; 2 | ALTER TABLE `apps` ADD `platform` TINYINT UNSIGNED NOT NULL DEFAULT 0; 3 | ALTER TABLE `packages` ADD `is_disabled` TINYINT UNSIGNED NOT NULL DEFAULT 0; 4 | ALTER TABLE `packages` ADD `rollout` TINYINT UNSIGNED NOT NULL DEFAULT 100; 5 | 6 | CREATE TABLE `log_report_deploy` ( 7 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 8 | `status` tinyint(3) unsigned NOT NULL DEFAULT '0', 9 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 10 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 11 | `previous_label` varchar(20) NOT NULL DEFAULT '', 12 | `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', 13 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 16 | 17 | CREATE TABLE `log_report_download` ( 18 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 19 | `package_id` int(10) unsigned NOT NULL DEFAULT '0', 20 | `client_unique_id` varchar(100) NOT NULL DEFAULT '', 21 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 22 | PRIMARY KEY (`id`) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 24 | 25 | UPDATE `versions` SET `version` = '0.3.0' WHERE `type` = '1'; -------------------------------------------------------------------------------- /sql/codepush-v0.4.0-patch.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `deployments_versions` ADD `min_version` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0'; 2 | ALTER TABLE `deployments_versions` ADD `max_version` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0'; 3 | ALTER TABLE `deployments_versions` ADD INDEX `idx_did_min_version` (`deployment_id`, `min_version`); 4 | ALTER TABLE `deployments_versions` ADD INDEX `idx_did_maxversion` (`deployment_id`, `max_version`); 5 | ALTER TABLE `deployments_versions` CHANGE `app_version` `app_version` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT ''; 6 | ALTER TABLE `deployments_versions` DROP INDEX `idx_did_appversion`; 7 | ALTER TABLE `deployments_versions` ADD INDEX `idx_did_appversion` (`deployment_id`, `app_version` (30)); 8 | 9 | UPDATE `versions` SET `version` = '0.4.0' WHERE `type` = '1'; -------------------------------------------------------------------------------- /sql/codepush-v0.5.0-patch.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `apps` ADD `is_use_diff_text` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0'; 2 | 3 | UPDATE `versions` SET `version` = '0.5.0' WHERE `type` = '1'; -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import bodyParser from 'body-parser'; 4 | import cookieParser from 'cookie-parser'; 5 | import express, { NextFunction } from 'express'; 6 | import helmet from 'helmet'; 7 | import { logger } from 'kv-logger'; 8 | import { AppError, NotFound } from './core/app-error'; 9 | import { config } from './core/config'; 10 | import { i18n } from './core/i18n'; 11 | import { Req, Res, withLogger } from './core/middleware'; 12 | import { accessKeysRouter } from './routes/accessKeys'; 13 | import { accountRouter } from './routes/account'; 14 | import { appsRouter } from './routes/apps'; 15 | import { authRouter } from './routes/auth'; 16 | import { indexRouter } from './routes/index'; 17 | import { indexV1Router } from './routes/indexV1'; 18 | import { usersRouter } from './routes/users'; 19 | 20 | export const app = express(); 21 | 22 | app.use( 23 | helmet({ 24 | contentSecurityPolicy: false, 25 | }), 26 | ); 27 | 28 | // view engine setup 29 | app.set('views', path.join(__dirname, '../views')); 30 | app.set('view engine', 'pug'); 31 | 32 | // translations 33 | app.use(i18n.init); 34 | 35 | app.use(bodyParser.json()); 36 | app.use(bodyParser.urlencoded({ extended: false })); 37 | app.use(cookieParser()); 38 | app.use(express.static(path.join(__dirname, '../public'))); 39 | app.use(withLogger); 40 | 41 | app.all('*', (req, res, next) => { 42 | res.header('Access-Control-Allow-Origin', '*'); 43 | res.header( 44 | 'Access-Control-Allow-Headers', 45 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CodePush-Plugin-Version, X-CodePush-Plugin-Name, X-CodePush-SDK-Version, X-Request-Id', 46 | ); 47 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,PATCH,DELETE,OPTIONS'); 48 | next(); 49 | }); 50 | 51 | logger.debug(`config common.storageType value: ${config.common.storageType}`); 52 | 53 | // config local storage 54 | if (config.common.storageType === 'local') { 55 | const localStorageDir = config.local.storageDir; 56 | if (localStorageDir) { 57 | logger.debug(`config common.storageDir value: ${localStorageDir}`); 58 | 59 | if (!fs.existsSync(localStorageDir)) { 60 | const e = new Error(`Please create dir ${localStorageDir}`); 61 | logger.error(e); 62 | throw e; 63 | } 64 | try { 65 | logger.debug('checking storageDir fs.W_OK | fs.R_OK'); 66 | // eslint-disable-next-line no-bitwise 67 | fs.accessSync(localStorageDir, fs.constants.W_OK | fs.constants.R_OK); 68 | logger.debug('storageDir fs.W_OK | fs.R_OK is ok'); 69 | } catch (e) { 70 | logger.error(e); 71 | throw e; 72 | } 73 | logger.debug(`static download uri value: ${config.local.public}`); 74 | app.use(config.local.public, express.static(localStorageDir)); 75 | } else { 76 | logger.error('please config local storageDir'); 77 | } 78 | } 79 | 80 | // config routes 81 | // code-push-client routes 82 | app.use('/', indexRouter); 83 | app.use('/v0.1/public/codepush', indexV1Router); 84 | // code-push-cli routes 85 | app.use('/accessKeys', accessKeysRouter); 86 | app.use('/apps', appsRouter); 87 | app.use('/account', accountRouter); 88 | // code-push-server routes 89 | app.use('/auth', authRouter); 90 | app.use('/users', usersRouter); 91 | 92 | // 404 handler 93 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 94 | app.use((req, res, next) => { 95 | throw new NotFound(`${req.method} ${req.url} not found`); 96 | }); 97 | 98 | // error handler 99 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 100 | app.use((err: Error, req: Req, res: Res, next: NextFunction) => { 101 | const thisLogger = req.logger || logger; 102 | if (err instanceof AppError) { 103 | res.status(err.status).send(err.message); 104 | thisLogger.debug(err); 105 | } else { 106 | res.status(500).send(err.message); 107 | thisLogger.error(err); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /src/core/app-error.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | export class AppError extends Error { 3 | constructor(message: string | Error) { 4 | super(message instanceof Error ? message.message : message); 5 | this.name = 'AppError'; 6 | } 7 | 8 | public status = 200; 9 | } 10 | 11 | export class NotFound extends AppError { 12 | constructor(message?: string | Error) { 13 | super(message || 'Not Found'); 14 | this.name = 'NotFoundError'; 15 | } 16 | 17 | public status = 404; 18 | } 19 | 20 | export class Unauthorized extends AppError { 21 | constructor(message?: string | Error) { 22 | super(message || 'Unauthorized'); 23 | this.name = 'UnauthorizedError'; 24 | } 25 | 26 | public status = 401; 27 | } 28 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { 3 | setLogTransports, 4 | ConsoleLogTransport, 5 | logger, 6 | LogLevel, 7 | withLogLevelFilter, 8 | } from 'kv-logger'; 9 | 10 | function toBool(str: string): boolean { 11 | return str === 'true' || str === '1'; 12 | } 13 | 14 | function toNumber(str: string, defaultValue: number): number { 15 | const num = Number(str); 16 | if (Number.isNaN(num)) { 17 | return defaultValue; 18 | } 19 | return num; 20 | } 21 | 22 | export const config = { 23 | // Config for log 24 | log: { 25 | // debug, info, warn, error 26 | level: process.env.LOG_LEVEL || 'info', 27 | // text, json 28 | format: process.env.LOG_FORMAT || 'text', 29 | }, 30 | // Config for database, only support mysql. 31 | db: { 32 | username: process.env.RDS_USERNAME || 'root', 33 | password: process.env.RDS_PASSWORD || 'password', 34 | database: process.env.RDS_DATABASE || 'codepush', 35 | host: process.env.RDS_HOST || '127.0.0.1', 36 | port: toNumber(process.env.RDS_PORT, 3306), 37 | dialect: 'mysql', 38 | logging: false, 39 | }, 40 | // Config for qiniu (http://www.qiniu.com/) cloud storage when storageType value is "qiniu". 41 | qiniu: { 42 | accessKey: process.env.QINIU_ACCESS_KEY, 43 | secretKey: process.env.QINIU_SECRET_KEY, 44 | bucketName: process.env.QINIU_BUCKET_NAME, 45 | downloadUrl: process.env.QINIU_DOWNLOAD_URL || process.env.DOWNLOAD_URL, 46 | }, 47 | // Config for Amazon s3 (https://aws.amazon.com/cn/s3/) storage when storageType value is "s3". 48 | s3: { 49 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 50 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 51 | sessionToken: process.env.AWS_SESSION_TOKEN, // (optional) 52 | bucketName: process.env.AWS_BUCKET_NAME, 53 | region: process.env.AWS_REGION, 54 | // binary files download host address. 55 | downloadUrl: process.env.AWS_DOWNLOAD_URL || process.env.DOWNLOAD_URL, 56 | }, 57 | // Config for Aliyun OSS (https://www.aliyun.com/product/oss) when storageType value is "oss". 58 | oss: { 59 | accessKeyId: process.env.OSS_ACCESS_KEY_ID, 60 | secretAccessKey: process.env.OSS_SECRET_ACCESS_KEY, 61 | endpoint: process.env.OSS_ENDPOINT, 62 | bucketName: process.env.OSS_BUCKET_NAME, 63 | // Key prefix in object key 64 | prefix: process.env.OSS_PREFIX, 65 | // binary files download host address 66 | downloadUrl: process.env.OSS_DOWNLOAD_URL || process.env.DOWNLOAD_URL, 67 | }, 68 | // Config for tencentyun COS (https://cloud.tencent.com/product/cos) when storageType value is "oss". 69 | tencentcloud: { 70 | accessKeyId: process.env.COS_ACCESS_KEY_ID, 71 | secretAccessKey: process.env.COS_SECRET_ACCESS_KEY, 72 | bucketName: process.env.COS_BUCKET_NAME, 73 | region: process.env.COS_REGION, 74 | // binary files download host address 75 | downloadUrl: process.env.COS_DOWNLOAD_URL || process.env.DOWNLOAD_URL, 76 | }, 77 | // Config for local storage when storageType value is "local". 78 | local: { 79 | // Binary files storage dir, Do not use tmpdir and its public download dir. 80 | storageDir: process.env.STORAGE_DIR || os.tmpdir(), 81 | // Binary files download host address which Code Push Server listen to. the files storage in storageDir. 82 | downloadUrl: 83 | process.env.LOCAL_DOWNLOAD_URL || 84 | process.env.DOWNLOAD_URL || 85 | 'http://127.0.0.1:3000/download', 86 | // public static download spacename. 87 | public: '/download', 88 | }, 89 | jwt: { 90 | // Recommended: 63 random alpha-numeric characters 91 | // Generate using: https://www.grc.com/passwords.htm 92 | tokenSecret: process.env.TOKEN_SECRET || 'INSERT_RANDOM_TOKEN_KEY', 93 | }, 94 | common: { 95 | // determine whether new account registrations are allowed 96 | allowRegistration: toBool(process.env.ALLOW_REGISTRATION), 97 | /* 98 | * tryLoginTimes limits login error attempts to avoid force attack. 99 | * if value is 0, no limit for login auth, it may not be safe. 100 | * when it's a number, it means you can try that many times today, 101 | * but it needs a redis server. 102 | */ 103 | tryLoginTimes: toNumber(process.env.TRY_LOGIN_TIMES, 4), 104 | // create patch updates's number. default value is 3 105 | diffNums: toNumber(process.env.DIFF_NUMS, 3), 106 | // data dir to calculate diff files. it's optimization. 107 | dataDir: process.env.DATA_DIR || os.tmpdir(), 108 | // storageType which is your binary package files store. options value is ("local" | "qiniu" | "s3"| "oss" || "tencentcloud") 109 | storageType: (process.env.STORAGE_TYPE || 'local') as 110 | | 'local' 111 | | 'qiniu' 112 | | 's3' 113 | | 'oss' 114 | | 'tencentcloud', 115 | // options value is (true | false), when it's true, it will cache updateCheck results in redis. 116 | updateCheckCache: toBool(process.env.UPDATE_CHECK_CACHE), 117 | // options value is (true | false), when it's true, it will cache rollout results in redis 118 | rolloutClientUniqueIdCache: toBool(process.env.ROLLOUT_CLIENT_UNIQUE_ID_CACHE), 119 | }, 120 | // Config for smtp email; register module needs to validate user email 121 | // project source https://github.com/nodemailer/nodemailer 122 | smtpConfig: { 123 | host: process.env.SMTP_HOST, 124 | port: toNumber(process.env.SMTP_PORT, 465), 125 | secure: true, 126 | auth: { 127 | user: process.env.SMTP_USERNAME, 128 | pass: process.env.SMTP_PASSWORD, 129 | }, 130 | }, 131 | // Config for redis (register module, tryLoginTimes module) 132 | redis: { 133 | host: process.env.REDIS_HOST || '127.0.0.1', 134 | port: toNumber(process.env.REDIS_PORT, 6379), 135 | password: process.env.REDIS_PASSWORD, 136 | db: toNumber(process.env.REDIS_DB, 0), 137 | }, 138 | } as const; 139 | 140 | // config logger - make sure its ready before anyting else 141 | setLogTransports( 142 | withLogLevelFilter(config.log.level as LogLevel)( 143 | new ConsoleLogTransport(config.log.format as 'text' | 'json'), 144 | ), 145 | ); 146 | 147 | const env = process.env.NODE_ENV || 'development'; 148 | logger.info(`use config`, { 149 | env, 150 | storageType: config.common.storageType, 151 | }); 152 | -------------------------------------------------------------------------------- /src/core/const.ts: -------------------------------------------------------------------------------- 1 | // supported platforms 2 | // TODO: refactor to enums and put name mapping into util method 3 | export const IOS = 1; 4 | export const IOS_NAME = 'iOS'; 5 | export const ANDROID = 2; 6 | export const ANDROID_NAME = 'Android'; 7 | export const WINDOWS = 3; 8 | export const WINDOWS_NAME = 'Windows'; 9 | 10 | // supported apps 11 | export const REACT_NATIVE = 1; 12 | export const REACT_NATIVE_NAME = 'React-Native'; 13 | export const CORDOVA = 2; 14 | export const CORDOVA_NAME = 'Cordova'; 15 | 16 | // supported env 17 | export const PRODUCTION = 'Production'; 18 | export const STAGING = 'Staging'; 19 | 20 | // flags 21 | export const IS_MANDATORY_YES = 1; 22 | export const IS_MANDATORY_NO = 0; 23 | 24 | export const IS_DISABLED_YES = 1; 25 | export const IS_DISABLED_NO = 0; 26 | 27 | export const DEPLOYMENT_SUCCEEDED = 1; 28 | export const DEPLOYMENT_FAILED = 2; 29 | 30 | export const RELEASE_METHOD_PROMOTE = 'Promote'; 31 | export const RELEASE_METHOD_UPLOAD = 'Upload'; 32 | 33 | export const DIFF_MANIFEST_FILE_NAME = 'hotcodepush.json'; 34 | 35 | export const CURRENT_DB_VERSION = '0.5.0'; 36 | -------------------------------------------------------------------------------- /src/core/i18n.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { I18n } from 'i18n'; 3 | 4 | export const i18n = new I18n(); 5 | 6 | i18n.configure({ 7 | directory: path.join(__dirname, '../../locales'), 8 | defaultLocale: 'en', 9 | }); 10 | -------------------------------------------------------------------------------- /src/core/middleware.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import type { Request, Response, NextFunction } from 'express'; 3 | import jwt from 'jsonwebtoken'; 4 | import { logger, Logger } from 'kv-logger'; 5 | import _ from 'lodash'; 6 | import moment from 'moment'; 7 | import { Op } from 'sequelize'; 8 | import { UserTokens } from '../models/user_tokens'; 9 | import { Users, UsersInterface } from '../models/users'; 10 | import { AppError, Unauthorized } from './app-error'; 11 | import { config } from './config'; 12 | import { parseToken, md5 } from './utils/security'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export interface Req

, B = any, Q = Record> 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | extends Request> { 18 | users: UsersInterface; 19 | logger: Logger; 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-explicit-any 23 | export interface Res extends Response {} 24 | 25 | /** 26 | * bind logger to request 27 | */ 28 | export function withLogger(req: Req, res: Res, next: NextFunction) { 29 | const { method, path, headers } = req; 30 | const requestId = headers['x-request-id'] || randomUUID(); 31 | req.logger = logger.bindContext({ 32 | path, 33 | method, 34 | requestId, 35 | }); 36 | res.header('X-Request-Id', requestId); 37 | next(); 38 | } 39 | 40 | async function checkAuthToken(authToken: string) { 41 | const objToken = parseToken(authToken); 42 | const users = await Users.findOne({ 43 | where: { identical: objToken.identical }, 44 | }); 45 | if (_.isEmpty(users)) { 46 | throw new Unauthorized(); 47 | } 48 | 49 | const tokenInfo = await UserTokens.findOne({ 50 | where: { 51 | tokens: authToken, 52 | uid: users.id, 53 | expires_at: { 54 | [Op.gt]: moment().format('YYYY-MM-DD HH:mm:ss'), 55 | }, 56 | }, 57 | }); 58 | if (_.isEmpty(tokenInfo)) { 59 | throw new Unauthorized(); 60 | } 61 | 62 | return users; 63 | } 64 | 65 | async function checkAccessToken(accessToken: string) { 66 | if (_.isEmpty(accessToken)) { 67 | throw new Unauthorized(); 68 | } 69 | 70 | let authData: { uid: number; hash: string }; 71 | try { 72 | authData = jwt.verify(accessToken, config.jwt.tokenSecret) as { 73 | uid: number; 74 | hash: string; 75 | }; 76 | } catch (e) { 77 | throw new Unauthorized(); 78 | } 79 | 80 | const { uid, hash } = authData; 81 | if (uid <= 0) { 82 | throw new Unauthorized(); 83 | } 84 | 85 | const users = await Users.findOne({ 86 | where: { id: uid }, 87 | }); 88 | if (_.isEmpty(users)) { 89 | throw new Unauthorized(); 90 | } 91 | 92 | if (hash !== md5(users.get('ack_code'))) { 93 | throw new Unauthorized(); 94 | } 95 | return users; 96 | } 97 | 98 | /** 99 | * check user token and bind user to request 100 | */ 101 | export function checkToken(req: Req, res: Res, next: NextFunction) { 102 | // get token and type 103 | let authType: 1 | 2 = 1; 104 | let authToken = ''; 105 | const authArr = _.split(req.get('Authorization'), ' '); 106 | if (authArr[0] === 'Bearer') { 107 | [, authToken] = authArr; // Bearer 108 | if (authToken && authToken.length > 64) { 109 | authType = 2; 110 | } else { 111 | authType = 1; 112 | } 113 | } else if (authArr[0] === 'Basic') { 114 | authType = 2; 115 | const b = Buffer.from(authArr[1], 'base64'); 116 | const user = _.split(b.toString(), ':'); 117 | [, authToken] = user; 118 | } 119 | 120 | // do check token 121 | let checkTokenResult: Promise; 122 | if (authToken && authType === 1) { 123 | checkTokenResult = checkAuthToken(authToken); 124 | } else if (authToken && authType === 2) { 125 | checkTokenResult = checkAccessToken(authToken); 126 | } else { 127 | res.send(new Unauthorized(`Auth type not supported.`)); 128 | return; 129 | } 130 | 131 | checkTokenResult 132 | .then((users) => { 133 | req.users = users; 134 | next(); 135 | }) 136 | .catch((e) => { 137 | if (e instanceof AppError) { 138 | res.status(e.status || 404).send(e.message); 139 | } else { 140 | next(e); 141 | } 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /src/core/services/app-manager.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Op } from 'sequelize'; 3 | import { Apps, AppsInterface } from '../../models/apps'; 4 | import { Collaborators } from '../../models/collaborators'; 5 | import { Deployments } from '../../models/deployments'; 6 | import { Users } from '../../models/users'; 7 | import { AppError } from '../app-error'; 8 | import { 9 | IOS, 10 | IOS_NAME, 11 | ANDROID, 12 | ANDROID_NAME, 13 | WINDOWS, 14 | WINDOWS_NAME, 15 | CORDOVA, 16 | CORDOVA_NAME, 17 | REACT_NATIVE, 18 | REACT_NATIVE_NAME, 19 | STAGING, 20 | PRODUCTION, 21 | } from '../const'; 22 | import { sequelize } from '../utils/connections'; 23 | import { randToken } from '../utils/security'; 24 | 25 | class AppManager { 26 | findAppByName(uid: number, appName: string) { 27 | return Apps.findOne({ where: { name: appName, uid } }); 28 | } 29 | 30 | addApp(uid: number, appName: string, os, platform, identical: string) { 31 | return sequelize.transaction((t) => { 32 | return Apps.create( 33 | { 34 | name: appName, 35 | uid, 36 | os, 37 | platform, 38 | }, 39 | { 40 | transaction: t, 41 | }, 42 | ).then((apps) => { 43 | const appId = apps.id; 44 | const deployments = []; 45 | let deploymentKey = randToken(28) + identical; 46 | deployments.push({ 47 | appid: appId, 48 | name: PRODUCTION, 49 | last_deployment_version_id: 0, 50 | label_id: 0, 51 | deployment_key: deploymentKey, 52 | }); 53 | deploymentKey = randToken(28) + identical; 54 | deployments.push({ 55 | appid: appId, 56 | name: STAGING, 57 | last_deployment_version_id: 0, 58 | label_id: 0, 59 | deployment_key: deploymentKey, 60 | }); 61 | return Promise.all([ 62 | Collaborators.create({ appid: appId, uid, roles: 'Owner' }, { transaction: t }), 63 | Deployments.bulkCreate(deployments, { transaction: t }), 64 | ]); 65 | }); 66 | }); 67 | } 68 | 69 | deleteApp(appId) { 70 | return sequelize.transaction((t) => { 71 | return Promise.all([ 72 | Apps.destroy({ where: { id: appId }, transaction: t }), 73 | Collaborators.destroy({ where: { appid: appId }, transaction: t }), 74 | Deployments.destroy({ where: { appid: appId }, transaction: t }), 75 | ]); 76 | }); 77 | } 78 | 79 | modifyApp(appId, params) { 80 | return Apps.update(params, { where: { id: appId } }).then(([affectedCount]) => { 81 | if (!_.gt(affectedCount, 0)) { 82 | throw new AppError('modify errors'); 83 | } 84 | return affectedCount; 85 | }); 86 | } 87 | 88 | transferApp(appId: number, fromUid: number, toUid: number) { 89 | return sequelize.transaction((t) => { 90 | return Promise.all([ 91 | Apps.update({ uid: toUid }, { where: { id: appId }, transaction: t }), 92 | Collaborators.destroy({ where: { appid: appId, uid: fromUid }, transaction: t }), 93 | Collaborators.destroy({ where: { appid: appId, uid: toUid }, transaction: t }), 94 | Collaborators.create( 95 | { appid: appId, uid: toUid, roles: 'Owner' }, 96 | { transaction: t }, 97 | ), 98 | ]); 99 | }); 100 | } 101 | 102 | listApps(uid: number) { 103 | return Collaborators.findAll({ where: { uid } }) 104 | .then((data) => { 105 | if (_.isEmpty(data)) { 106 | return [] as AppsInterface[]; 107 | } 108 | const appIds = _.map(data, (v) => { 109 | return v.appid; 110 | }); 111 | return Apps.findAll({ where: { id: { [Op.in]: appIds } } }); 112 | }) 113 | .then((appInfos) => { 114 | const rs = Promise.all( 115 | _.values(appInfos).map((v) => { 116 | return this.getAppDetailInfo(v, uid).then((info) => { 117 | let os = ''; 118 | if (info.os === IOS) { 119 | os = IOS_NAME; 120 | } else if (info.os === ANDROID) { 121 | os = ANDROID_NAME; 122 | } else if (info.os === WINDOWS) { 123 | os = WINDOWS_NAME; 124 | } 125 | 126 | let platform = ''; 127 | if (info.platform === REACT_NATIVE) { 128 | platform = REACT_NATIVE_NAME; 129 | } else if (info.platform === CORDOVA) { 130 | platform = CORDOVA_NAME; 131 | } 132 | return { 133 | ...info, 134 | os, 135 | platform, 136 | }; 137 | }); 138 | }), 139 | ); 140 | return rs; 141 | }); 142 | } 143 | 144 | private getAppDetailInfo(appInfo: AppsInterface, currentUid: number) { 145 | const appId = appInfo.get('id'); 146 | return Promise.all([ 147 | Deployments.findAll({ where: { appid: appId } }), 148 | Collaborators.findAll({ where: { appid: appId } }).then((collaboratorInfos) => { 149 | return collaboratorInfos.reduce((prev, collaborator) => { 150 | return prev.then((allCol) => { 151 | return Users.findOne({ where: { id: collaborator.get('uid') } }).then( 152 | (u) => { 153 | let isCurrentAccount = false; 154 | if (_.eq(u.get('id'), currentUid)) { 155 | isCurrentAccount = true; 156 | } 157 | allCol[u.get('email')] = { 158 | permission: collaborator.get('roles'), 159 | isCurrentAccount, 160 | }; 161 | return allCol; 162 | }, 163 | ); 164 | }); 165 | }, Promise.resolve({})); 166 | }), 167 | ]).then(([deploymentInfos, collaborators]) => { 168 | return { 169 | collaborators, 170 | deployments: _.map(deploymentInfos, (item) => { 171 | return _.get(item, 'name'); 172 | }), 173 | os: appInfo.get('os'), 174 | platform: appInfo.get('platform'), 175 | name: appInfo.get('name'), 176 | id: appInfo.get('id'), 177 | }; 178 | }); 179 | } 180 | } 181 | 182 | export const appManager = new AppManager(); 183 | -------------------------------------------------------------------------------- /src/core/services/collaborators-manager.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Op } from 'sequelize'; 3 | import { Collaborators, CollaboratorsInterface } from '../../models/collaborators'; 4 | import { Users } from '../../models/users'; 5 | import { AppError } from '../app-error'; 6 | 7 | class CollaboratorsManager { 8 | listCollaborators(appId: number) { 9 | return Collaborators.findAll({ where: { appid: appId } }) 10 | .then( 11 | ( 12 | data, 13 | ): { 14 | uids: number[]; 15 | colByUid: Record; 16 | } => { 17 | return _.reduce( 18 | data, 19 | (result, value) => { 20 | result.uids.push(value.uid); 21 | result.colByUid[value.uid] = value; 22 | return result; 23 | }, 24 | { 25 | uids: [], 26 | colByUid: {}, 27 | }, 28 | ); 29 | }, 30 | ) 31 | .then((coInfo) => { 32 | return Users.findAll({ where: { id: { [Op.in]: coInfo.uids } } }).then((data2) => { 33 | return _.reduce( 34 | data2, 35 | (result, value) => { 36 | let permission = ''; 37 | if (!_.isEmpty(coInfo.colByUid[value.id])) { 38 | permission = coInfo.colByUid[value.id].roles; 39 | } 40 | result[value.email] = { permission }; 41 | return result; 42 | }, 43 | {} as Record, 44 | ); 45 | }); 46 | }); 47 | } 48 | 49 | addCollaborator(appId: number, uid: number) { 50 | return Collaborators.findOne({ where: { appid: appId, uid } }).then((data) => { 51 | if (_.isEmpty(data)) { 52 | return Collaborators.create({ 53 | appid: appId, 54 | uid, 55 | roles: 'Collaborator', 56 | }); 57 | } 58 | throw new AppError('user already is Collaborator.'); 59 | }); 60 | } 61 | 62 | deleteCollaborator(appId: number, uid: number) { 63 | return Collaborators.findOne({ where: { appid: appId, uid } }).then((data) => { 64 | if (_.isEmpty(data)) { 65 | throw new AppError('user is not a Collaborator'); 66 | } else { 67 | return Collaborators.destroy({ where: { id: data.id } }); 68 | } 69 | }); 70 | } 71 | } 72 | 73 | export const collaboratorsManager = new CollaboratorsManager(); 74 | -------------------------------------------------------------------------------- /src/core/services/datacenter-manager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { Logger } from 'kv-logger'; 4 | import _ from 'lodash'; 5 | import { AppError } from '../app-error'; 6 | import { config } from '../config'; 7 | import { createEmptyFolder, copy } from '../utils/common'; 8 | import { calcAllFileSha256, packageHashSync } from '../utils/security'; 9 | 10 | const MANIFEST_FILENAME = 'manifest.json'; 11 | const CONTENTS_NAME = 'contents'; 12 | 13 | class DataCenterManager { 14 | getDataDir() { 15 | return config.common.dataDir; 16 | } 17 | 18 | hasPackageStoreSync(packageHash: string) { 19 | const dataDir = this.getDataDir(); 20 | const packageHashPath = path.join(dataDir, packageHash); 21 | const manifestFile = path.join(packageHashPath, MANIFEST_FILENAME); 22 | const contentPath = path.join(packageHashPath, CONTENTS_NAME); 23 | return fs.existsSync(manifestFile) && fs.existsSync(contentPath); 24 | } 25 | 26 | getPackageInfo(packageHash: string) { 27 | if (this.hasPackageStoreSync(packageHash)) { 28 | const dataDir = this.getDataDir(); 29 | const packageHashPath = path.join(dataDir, packageHash); 30 | const manifestFile = path.join(packageHashPath, MANIFEST_FILENAME); 31 | const contentPath = path.join(packageHashPath, CONTENTS_NAME); 32 | return this.buildPackageInfo(packageHash, packageHashPath, contentPath, manifestFile); 33 | } 34 | throw new AppError("can't get PackageInfo"); 35 | } 36 | 37 | buildPackageInfo( 38 | packageHash: string, 39 | packageHashPath: string, 40 | contentPath: string, 41 | manifestFile: string, 42 | ) { 43 | return { 44 | packageHash, 45 | path: packageHashPath, 46 | contentPath, 47 | manifestFilePath: manifestFile, 48 | }; 49 | } 50 | 51 | validateStore(providePackageHash: string, logger: Logger) { 52 | const dataDir = this.getDataDir(); 53 | const packageHashPath = path.join(dataDir, providePackageHash); 54 | const manifestFile = path.join(packageHashPath, MANIFEST_FILENAME); 55 | const contentPath = path.join(packageHashPath, CONTENTS_NAME); 56 | if (!this.hasPackageStoreSync(providePackageHash)) { 57 | logger.debug(`validateStore providePackageHash not exist`); 58 | return Promise.resolve(false); 59 | } 60 | return calcAllFileSha256(contentPath).then((manifestJson) => { 61 | const packageHash = packageHashSync(manifestJson); 62 | let manifestJsonLocal; 63 | try { 64 | manifestJsonLocal = JSON.parse(fs.readFileSync(manifestFile, { encoding: 'utf8' })); 65 | } catch (e) { 66 | logger.debug(`validateStore manifestFile contents invilad`); 67 | return false; 68 | } 69 | const packageHashLocal = packageHashSync(manifestJsonLocal); 70 | if ( 71 | _.eq(providePackageHash, packageHash) && 72 | _.eq(providePackageHash, packageHashLocal) 73 | ) { 74 | logger.debug(`validateStore store files is ok`); 75 | return true; 76 | } 77 | logger.debug(`validateStore store files broken`); 78 | return false; 79 | }); 80 | } 81 | 82 | storePackage(sourceDst: string, force: boolean, logger: Logger) { 83 | return calcAllFileSha256(sourceDst).then((manifestJson) => { 84 | const packageHash = packageHashSync(manifestJson); 85 | const dataDir = this.getDataDir(); 86 | const packageHashPath = path.join(dataDir, packageHash); 87 | const manifestFile = path.join(packageHashPath, MANIFEST_FILENAME); 88 | const contentPath = path.join(packageHashPath, CONTENTS_NAME); 89 | return this.validateStore(packageHash, logger).then((isValidate) => { 90 | if (!force && isValidate) { 91 | return this.buildPackageInfo( 92 | packageHash, 93 | packageHashPath, 94 | contentPath, 95 | manifestFile, 96 | ); 97 | } 98 | return createEmptyFolder(packageHashPath).then(() => { 99 | return copy(sourceDst, contentPath).then(() => { 100 | const manifestString = JSON.stringify(manifestJson); 101 | fs.writeFileSync(manifestFile, manifestString); 102 | return this.buildPackageInfo( 103 | packageHash, 104 | packageHashPath, 105 | contentPath, 106 | manifestFile, 107 | ); 108 | }); 109 | }); 110 | }); 111 | }); 112 | } 113 | } 114 | 115 | export const dataCenterManager = new DataCenterManager(); 116 | -------------------------------------------------------------------------------- /src/core/services/email-manager.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import nodemailer from 'nodemailer'; 3 | import { AppError } from '../app-error'; 4 | import { config } from '../config'; 5 | 6 | class EmailManager { 7 | sendMail(options: { to: string; html: string; subject?: string; from?: string }) { 8 | return new Promise((resolve, reject) => { 9 | if (!_.get(options, 'to')) { 10 | reject(new AppError('to是必传参数')); 11 | return; 12 | } 13 | const { smtpConfig } = config; 14 | if (!smtpConfig || !smtpConfig.host) { 15 | resolve({}); 16 | return; 17 | } 18 | const transporter = nodemailer.createTransport(smtpConfig); 19 | const sendEmailAddress = smtpConfig.auth.user; 20 | const defaultMailOptions = { 21 | from: `"CodePush Server" <${sendEmailAddress}>`, // sender address 22 | to: '', // list of receivers 必传参数 23 | subject: 'CodePush Server', // Subject line 24 | html: '', // html body 25 | }; 26 | const mailOptions = _.assign(defaultMailOptions, options); 27 | transporter.sendMail(mailOptions, (error, info) => { 28 | if (error) { 29 | reject(error); 30 | return; 31 | } 32 | resolve(info); 33 | }); 34 | }); 35 | } 36 | 37 | sendRegisterCodeMail(email: string, code: string) { 38 | return this.sendMail({ 39 | to: email, 40 | html: `

您接收的验证码为: ${code} 20分钟内有效
`, 41 | }); 42 | } 43 | } 44 | 45 | export const emailManager = new EmailManager(); 46 | -------------------------------------------------------------------------------- /src/core/utils/common.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-cond-assign */ 2 | import fs from 'fs'; 3 | import { pipeline } from 'stream'; 4 | import util from 'util'; 5 | import extract from 'extract-zip'; 6 | import fsextra from 'fs-extra'; 7 | import { Logger } from 'kv-logger'; 8 | import _ from 'lodash'; 9 | import fetch from 'node-fetch'; 10 | import validator from 'validator'; 11 | import { AppError } from '../app-error'; 12 | import { config } from '../config'; 13 | 14 | const streamPipeline = util.promisify(pipeline); 15 | 16 | export function parseVersion(versionNo: string) { 17 | let version = '0'; 18 | let data = null; 19 | if ((data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/))) { 20 | // "1.2.3" 21 | version = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 22 | } else if ((data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5})$/))) { 23 | // "1.2" 24 | version = data[1] + _.padStart(data[2], 5, '0') + _.padStart('0', 10, '0'); 25 | } 26 | return version; 27 | } 28 | 29 | export function validatorVersion(versionNo: string) { 30 | let flag = false; 31 | let min = '0'; 32 | let max = '9999999999999999999'; 33 | let data = null; 34 | if (versionNo === '*') { 35 | // "*" 36 | flag = true; 37 | } else if ((data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/))) { 38 | // "1.2.3" 39 | flag = true; 40 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 41 | max = 42 | data[1] + 43 | _.padStart(data[2], 5, '0') + 44 | _.padStart(`${parseInt(data[3], 10) + 1}`, 10, '0'); 45 | } else if ((data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5})(\.\*){0,1}$/))) { 46 | // "1.2" "1.2.*" 47 | flag = true; 48 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart('0', 10, '0'); 49 | max = 50 | data[1] + _.padStart(`${parseInt(data[2], 10) + 1}`, 5, '0') + _.padStart('0', 10, '0'); 51 | } else if ((data = versionNo.match(/^~([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/))) { 52 | // "~1.2.3" 53 | flag = true; 54 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 55 | max = 56 | data[1] + _.padStart(`${parseInt(data[2], 10) + 1}`, 5, '0') + _.padStart('0', 10, '0'); 57 | } else if ((data = versionNo.match(/^\^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/))) { 58 | // "^1.2.3" 59 | flag = true; 60 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 61 | max = 62 | _.toString(parseInt(data[1], 10) + 1) + 63 | _.padStart('0', 5, '0') + 64 | _.padStart('0', 10, '0'); 65 | } else if ( 66 | (data = versionNo.match( 67 | /^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})\s?-\s?([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/, 68 | )) 69 | ) { 70 | // "1.2.3 - 1.2.7" 71 | flag = true; 72 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 73 | max = 74 | data[4] + 75 | _.padStart(data[5], 5, '0') + 76 | _.padStart(`${parseInt(data[6], 10) + 1}`, 10, '0'); 77 | } else if ( 78 | (data = versionNo.match( 79 | /^>=([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})\s?<([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/, 80 | )) 81 | ) { 82 | // ">=1.2.3 <1.2.7" 83 | flag = true; 84 | min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); 85 | max = data[4] + _.padStart(data[5], 5, '0') + _.padStart(data[6], 10, '0'); 86 | } 87 | return [flag, min, max]; 88 | } 89 | 90 | export async function createFileFromRequest(url: string, filePath: string, logger: Logger) { 91 | try { 92 | await fs.promises.stat(filePath); 93 | return; 94 | } catch (err) { 95 | if (err.code !== 'ENOENT') { 96 | throw err; 97 | } 98 | } 99 | 100 | logger.debug(`createFileFromRequest url:${url}`); 101 | const response = await fetch(url); 102 | if (!response.ok) { 103 | throw new AppError(`unexpected response ${response.statusText}`); 104 | } 105 | await streamPipeline(response.body, fs.createWriteStream(filePath)); 106 | } 107 | 108 | export function copySync(sourceDst: string, targertDst: string) { 109 | return fsextra.copySync(sourceDst, targertDst, { overwrite: true }); 110 | } 111 | 112 | export function copy(sourceDst: string, targertDst: string) { 113 | return fsextra.copy(sourceDst, targertDst, { overwrite: true }); 114 | } 115 | 116 | function deleteFolder(folderPath: string) { 117 | return fsextra.remove(folderPath); 118 | } 119 | 120 | export function deleteFolderSync(folderPath: string) { 121 | return fsextra.removeSync(folderPath); 122 | } 123 | 124 | export async function createEmptyFolder(folderPath: string) { 125 | await deleteFolder(folderPath); 126 | await fsextra.mkdirs(folderPath); 127 | } 128 | 129 | export function createEmptyFolderSync(folderPath: string) { 130 | deleteFolderSync(folderPath); 131 | fsextra.mkdirsSync(folderPath); 132 | } 133 | 134 | export async function unzipFile(zipFile: string, outputPath: string, logger: Logger) { 135 | try { 136 | logger.debug(`unzipFile check zipFile ${zipFile} fs.R_OK`); 137 | fs.accessSync(zipFile, fs.constants.R_OK); 138 | logger.debug(`Pass unzipFile file ${zipFile}`); 139 | } catch (err) { 140 | throw new AppError(err.message); 141 | } 142 | 143 | try { 144 | await extract(zipFile, { dir: outputPath }); 145 | logger.debug(`unzipFile success`); 146 | } catch (err) { 147 | throw new AppError(`it's not a zipFile`); 148 | } 149 | return outputPath; 150 | } 151 | 152 | export function getBlobDownloadUrl(blobUrl: string): string { 153 | let fileName = blobUrl; 154 | const { storageType } = config.common; 155 | const { downloadUrl } = config[storageType]; 156 | if (storageType === 'local') { 157 | fileName = `${blobUrl.substring(0, 2).toLowerCase()}/${blobUrl}`; 158 | } 159 | if (!validator.isURL(downloadUrl)) { 160 | throw new AppError(`Please config ${storageType}.downloadUrl in config.js`); 161 | } 162 | return `${downloadUrl}/${fileName}`; 163 | } 164 | 165 | export function diffCollectionsSync( 166 | collection1: Record, 167 | collection2: Record, 168 | ) { 169 | const diff: string[] = []; 170 | const collection1Only: string[] = []; 171 | const collection2Keys = new Set(Object.keys(collection2)); 172 | if (collection1 instanceof Object) { 173 | const keys = Object.keys(collection1); 174 | for (let i = 0; i < keys.length; i += 1) { 175 | const key = keys[i]; 176 | if (!collection2Keys.has(key)) { 177 | collection1Only.push(key); 178 | } else { 179 | collection2Keys.delete(key); 180 | if (!_.eq(collection1[key], collection2[key])) { 181 | diff.push(key); 182 | } 183 | } 184 | } 185 | } 186 | return { 187 | diff, 188 | collection1Only, 189 | collection2Only: Array.from(collection2Keys), 190 | }; 191 | } 192 | -------------------------------------------------------------------------------- /src/core/utils/connections.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | import { Sequelize } from 'sequelize'; 3 | import { config } from '../config'; 4 | 5 | export const sequelize = new Sequelize( 6 | config.db.database, 7 | config.db.username, 8 | config.db.password, 9 | config.db, 10 | ); 11 | 12 | export const redisClient = createClient({ 13 | socket: { 14 | host: config.redis.host, 15 | port: config.redis.port, 16 | reconnectStrategy: (retries) => { 17 | if (retries > 10) { 18 | return new Error('Retry count exhausted'); 19 | } 20 | 21 | return retries * 100; 22 | }, 23 | }, 24 | password: config.redis.password, 25 | database: config.redis.db, 26 | }); 27 | redisClient.connect(); 28 | -------------------------------------------------------------------------------- /src/core/utils/qetag.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import crypto from 'crypto'; 3 | import fs from 'fs'; 4 | import { Stream, Readable } from 'stream'; 5 | import { Logger } from 'kv-logger'; 6 | import { AppError } from '../app-error'; 7 | 8 | // 计算文件的eTag,参数为buffer或者readableStream或者文件路径 9 | function getEtag(buffer: string | Stream | Buffer, callback: (etag: string) => void) { 10 | // 判断传入的参数是buffer还是stream还是filepath 11 | let mode = 'buffer'; 12 | 13 | if (typeof buffer === 'string') { 14 | // eslint-disable-next-line no-param-reassign 15 | buffer = fs.createReadStream(buffer); 16 | mode = 'stream'; 17 | } else if (buffer instanceof Stream) { 18 | mode = 'stream'; 19 | } 20 | 21 | // sha1算法 22 | const sha1 = (content) => { 23 | const sha1Hash = crypto.createHash('sha1'); 24 | sha1Hash.update(content); 25 | return sha1Hash.digest(); 26 | }; 27 | 28 | // 以4M为单位分割 29 | const blockSize = 4 * 1024 * 1024; 30 | const sha1String = []; 31 | let prefix = 0x16; 32 | let blockCount = 0; 33 | 34 | const calcEtag = () => { 35 | if (!sha1String.length) { 36 | return 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ'; 37 | } 38 | let sha1Buffer = Buffer.concat(sha1String, blockCount * 20); 39 | 40 | // 如果大于4M,则对各个块的sha1结果再次sha1 41 | if (blockCount > 1) { 42 | prefix = 0x96; 43 | sha1Buffer = sha1(sha1Buffer); 44 | } 45 | 46 | sha1Buffer = Buffer.concat([Buffer.from([prefix]), sha1Buffer], sha1Buffer.length + 1); 47 | 48 | return sha1Buffer.toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); 49 | }; 50 | 51 | switch (mode) { 52 | case 'buffer': { 53 | const buf = buffer as Buffer; 54 | const bufferSize = buf.length; 55 | blockCount = Math.ceil(bufferSize / blockSize); 56 | 57 | for (let i = 0; i < blockCount; i += 1) { 58 | sha1String.push(sha1(buf.slice(i * blockSize, (i + 1) * blockSize))); 59 | } 60 | process.nextTick(() => { 61 | callback(calcEtag()); 62 | }); 63 | break; 64 | } 65 | case 'stream': { 66 | const stream = buffer as Readable; 67 | stream.on('readable', () => { 68 | for (;;) { 69 | const chunk = stream.read(blockSize); 70 | if (!chunk) { 71 | break; 72 | } 73 | sha1String.push(sha1(chunk)); 74 | blockCount += 1; 75 | } 76 | }); 77 | stream.on('end', () => { 78 | callback(calcEtag()); 79 | }); 80 | 81 | break; 82 | } 83 | default: 84 | // cannot be here 85 | break; 86 | } 87 | } 88 | 89 | // TODO: support only files (string)? 90 | export function qetag(buffer: string | Stream | Buffer, logger: Logger): Promise { 91 | if (typeof buffer === 'string') { 92 | // it's a file 93 | try { 94 | logger.debug(`Check upload file ${buffer} fs.R_OK`); 95 | fs.accessSync(buffer, fs.constants.R_OK); 96 | logger.debug(`Check upload file ${buffer} fs.R_OK pass`); 97 | } catch (e) { 98 | logger.error(e); 99 | return Promise.reject(new AppError(e.message)); 100 | } 101 | } 102 | logger.debug(`generate file identical`); 103 | return new Promise((resolve) => { 104 | getEtag(buffer, (data) => { 105 | if (typeof buffer === 'string') { 106 | logger.debug('identical:', { 107 | file: buffer, 108 | etag: data, 109 | }); 110 | } 111 | resolve(data); 112 | }); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /src/core/utils/security.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import bcrypt from 'bcryptjs'; 5 | import { logger } from 'kv-logger'; 6 | import _ from 'lodash'; 7 | import { generator } from 'rand-token'; 8 | import recursive from 'recursive-readdir'; 9 | import slash from 'slash'; 10 | import { AppError } from '../app-error'; 11 | import { ANDROID, IOS } from '../const'; 12 | 13 | export function md5(str: string) { 14 | const md5sum = crypto.createHash('md5'); 15 | md5sum.update(str); 16 | return md5sum.digest('hex'); 17 | } 18 | 19 | export function passwordHashSync(password: string) { 20 | return bcrypt.hashSync(password, bcrypt.genSaltSync(12)); 21 | } 22 | 23 | export function passwordVerifySync(password: string, hash: string) { 24 | return bcrypt.compareSync(password, hash); 25 | } 26 | 27 | const randTokenGen = generator({ 28 | chars: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 29 | source: 'crypto', 30 | }); 31 | 32 | export function randToken(num: number) { 33 | return randTokenGen.generate(num); 34 | } 35 | 36 | export function parseToken(token: string) { 37 | return { identical: token.substring(token.length - 9), token: token.substring(0, 28) }; 38 | } 39 | 40 | function fileSha256(file: string): Promise { 41 | return new Promise((resolve, reject) => { 42 | const rs = fs.createReadStream(file); 43 | const hash = crypto.createHash('sha256'); 44 | rs.on('data', hash.update.bind(hash)); 45 | rs.on('error', (e) => { 46 | reject(e); 47 | }); 48 | rs.on('end', () => { 49 | resolve(hash.digest('hex')); 50 | }); 51 | }); 52 | } 53 | 54 | function stringSha256Sync(contents: string) { 55 | const sha256 = crypto.createHash('sha256'); 56 | sha256.update(contents); 57 | return sha256.digest('hex'); 58 | } 59 | 60 | function sortJsonToArr(json: Record) { 61 | const rs: { path: string; hash: string }[] = []; 62 | _.forIn(json, (value, key) => { 63 | rs.push({ path: key, hash: value }); 64 | }); 65 | return _.sortBy(rs, (o) => o.path); 66 | } 67 | 68 | // some files are ignored in calc hash in client sdk 69 | // https://github.com/Microsoft/react-native-code-push/pull/974/files#diff-21b650f88429c071b217d46243875987R15 70 | function isHashIgnored(relativePath: string) { 71 | if (!relativePath) { 72 | return true; 73 | } 74 | 75 | const IgnoreMacOSX = '__MACOSX/'; 76 | const IgnoreDSStore = '.DS_Store'; 77 | 78 | return ( 79 | relativePath.startsWith(IgnoreMacOSX) || 80 | relativePath === IgnoreDSStore || 81 | relativePath.endsWith(IgnoreDSStore) 82 | ); 83 | } 84 | 85 | function isPackageHashIgnored(relativePath: string) { 86 | if (!relativePath) { 87 | return true; 88 | } 89 | 90 | // .codepushrelease contains code sign JWT 91 | // it should be ignored in package hash but need to be included in package manifest 92 | const IgnoreCodePushMetadata = '.codepushrelease'; 93 | return ( 94 | relativePath === IgnoreCodePushMetadata || 95 | relativePath.endsWith(IgnoreCodePushMetadata) || 96 | isHashIgnored(relativePath) 97 | ); 98 | } 99 | 100 | export function packageHashSync(jsonData: Record) { 101 | const sortedArr = sortJsonToArr(jsonData); 102 | const manifestData = _.filter(sortedArr, (v) => { 103 | return !isPackageHashIgnored(v.path); 104 | }).map((v) => { 105 | return `${v.path}:${v.hash}`; 106 | }); 107 | let manifestString = JSON.stringify(manifestData.sort()); 108 | manifestString = _.replace(manifestString, /\\\//g, '/'); 109 | logger.debug('packageHashSync manifestString', { 110 | manifestString, 111 | }); 112 | return stringSha256Sync(manifestString); 113 | } 114 | 115 | function sha256AllFiles(files: string[]): Promise> { 116 | return new Promise((resolve) => { 117 | const results: Record = {}; 118 | const { length } = files; 119 | let count = 0; 120 | files.forEach((file) => { 121 | fileSha256(file).then((hash) => { 122 | results[file] = hash; 123 | count += 1; 124 | if (count === length) { 125 | resolve(results); 126 | } 127 | }); 128 | }); 129 | }); 130 | } 131 | 132 | export function uploadPackageType(directoryPath: string) { 133 | return new Promise((resolve, reject) => { 134 | recursive(directoryPath, (err, files) => { 135 | if (err) { 136 | logger.error(new AppError(err.message)); 137 | reject(new AppError(err.message)); 138 | } else if (files.length === 0) { 139 | logger.debug(`uploadPackageType empty files`); 140 | reject(new AppError('empty files')); 141 | } else { 142 | const aregex = /android\.bundle/; 143 | const aregexIOS = /main\.jsbundle/; 144 | let packageType = 0; 145 | _.forIn(files, (value: string) => { 146 | if (aregex.test(value)) { 147 | packageType = ANDROID; 148 | return false; 149 | } 150 | if (aregexIOS.test(value)) { 151 | packageType = IOS; 152 | return false; 153 | } 154 | 155 | return undefined; 156 | }); 157 | logger.debug(`uploadPackageType packageType: ${packageType}`); 158 | resolve(packageType); 159 | } 160 | }); 161 | }); 162 | } 163 | 164 | export function calcAllFileSha256(directoryPath: string): Promise> { 165 | return new Promise((resolve, reject) => { 166 | recursive(directoryPath, (error, files) => { 167 | if (error) { 168 | logger.error(error); 169 | reject(new AppError(error.message)); 170 | } else { 171 | // filter files that should be ignored 172 | // eslint-disable-next-line no-param-reassign 173 | files = files.filter((file) => { 174 | const relative = path.relative(directoryPath, file); 175 | return !isHashIgnored(relative); 176 | }); 177 | 178 | if (files.length === 0) { 179 | logger.debug(`calcAllFileSha256 empty files in directory`, { directoryPath }); 180 | reject(new AppError('empty files')); 181 | } else { 182 | sha256AllFiles(files).then((results) => { 183 | const data: Record = {}; 184 | _.forIn(results, (value, key) => { 185 | let relativePath = path.relative(directoryPath, key); 186 | relativePath = slash(relativePath); 187 | data[relativePath] = value; 188 | }); 189 | logger.debug(`calcAllFileSha256 files:`, data); 190 | resolve(data); 191 | }); 192 | } 193 | } 194 | }); 195 | }); 196 | } 197 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import _ from 'lodash'; 10 | import mysql from 'mysql2'; 11 | import yargs from 'yargs'; 12 | import { CURRENT_DB_VERSION } from './core/const'; 13 | 14 | const argv = yargs 15 | .usage('Usage: $0 [options]') 16 | .command('init', 'Initialize the database', { 17 | dbpassword: { 18 | alias: 'dbpassword', 19 | type: 'string', 20 | }, 21 | }) 22 | .command('upgrade', 'Upgrade the database', { 23 | dbpassword: { 24 | alias: 'dbpassword', 25 | type: 'string', 26 | }, 27 | }) 28 | .example( 29 | '$0 init --dbname codepush --dbhost localhost --dbuser root --dbpassword 123456 --dbport 3306 --force', 30 | 'Initialize the code-push-server database', 31 | ) 32 | .example( 33 | '$0 upgrade --dbname codepush --dbhost localhost --dbuser root --dbpassword 123456 --dbport 3306', 34 | 'Upgrade the code-push-server database', 35 | ) 36 | .default({ 37 | dbname: 'codepush', 38 | dbhost: 'localhost', 39 | dbuser: 'root', 40 | dbpassword: null, 41 | }) 42 | .help('h') 43 | .alias('h', 'help') 44 | .parseSync(); 45 | 46 | const command = argv._[0]; 47 | const dbname = argv.dbname ? argv.dbname : 'codepush'; 48 | const dbhost = argv.dbhost ? argv.dbhost : 'localhost'; 49 | const dbuser = argv.dbuser ? argv.dbuser : 'root'; 50 | const dbport = argv.dbport ? argv.dbport : 3306; 51 | const { dbpassword } = argv; 52 | 53 | if (command === 'init') { 54 | let connection2; 55 | const connection = mysql 56 | .createConnection({ 57 | host: dbhost, 58 | user: dbuser, 59 | password: dbpassword, 60 | port: dbport, 61 | }) 62 | .promise(); 63 | const createDatabaseSql = argv.force 64 | ? `CREATE DATABASE IF NOT EXISTS ${dbname}` 65 | : `CREATE DATABASE ${dbname}`; 66 | connection.connect(); 67 | connection 68 | .query(createDatabaseSql) 69 | .then(() => { 70 | connection2 = mysql 71 | .createConnection({ 72 | host: dbhost, 73 | user: dbuser, 74 | password: dbpassword, 75 | database: dbname, 76 | multipleStatements: true, 77 | port: dbport, 78 | }) 79 | .promise(); 80 | connection2.connect(); 81 | return connection2; 82 | }) 83 | .then(() => { 84 | const sql = fs.readFileSync( 85 | path.resolve(__dirname, '../sql/codepush-all.sql'), 86 | 'utf-8', 87 | ); 88 | return connection2.query(sql); 89 | }) 90 | .then(() => { 91 | console.log('success.'); 92 | }) 93 | .catch((e) => { 94 | console.log(e); 95 | }) 96 | .finally(() => { 97 | if (connection) connection.end(); 98 | if (connection2) connection2.end(); 99 | }); 100 | } else if (command === 'upgrade') { 101 | let connection; 102 | try { 103 | connection = mysql 104 | .createConnection({ 105 | host: dbhost, 106 | user: dbuser, 107 | password: dbpassword, 108 | database: dbname, 109 | multipleStatements: true, 110 | port: dbport, 111 | }) 112 | .promise(); 113 | connection.connect(); 114 | } catch (e) { 115 | console.error('connect mysql error, check params', e); 116 | process.exit(1); 117 | } 118 | 119 | let versionNo = '0.0.1'; 120 | connection 121 | .query('select `version` from `versions` where `type`=1 limit 1') 122 | .then((rs) => { 123 | versionNo = _.get(rs, '0.version', '0.0.1'); 124 | if (versionNo === CURRENT_DB_VERSION) { 125 | console.log('Everything up-to-date.'); 126 | process.exit(0); 127 | } 128 | const allSqlFile = [ 129 | { 130 | version: '0.2.14', 131 | path: path.resolve(__dirname, '../sql/codepush-v0.2.14-patch.sql'), 132 | }, 133 | { 134 | version: '0.2.15', 135 | path: path.resolve(__dirname, '../sql/codepush-v0.2.15-patch.sql'), 136 | }, 137 | { 138 | version: '0.3.0', 139 | path: path.resolve(__dirname, '../sql/codepush-v0.3.0-patch.sql'), 140 | }, 141 | { 142 | version: '0.4.0', 143 | path: path.resolve(__dirname, '../sql/codepush-v0.4.0-patch.sql'), 144 | }, 145 | { 146 | version: '0.5.0', 147 | path: path.resolve(__dirname, '../sql/codepush-v0.5.0-patch.sql'), 148 | }, 149 | ]; 150 | return allSqlFile.reduce((prev, sqlFile) => { 151 | if (!_.gt(sqlFile.version, versionNo)) { 152 | return prev; 153 | } 154 | const sql = fs.readFileSync(sqlFile.path, 'utf-8'); 155 | console.log(`exec sql file:${sqlFile.path}`); 156 | return connection.query(sql).then(() => { 157 | console.log(`success exec sql file:${sqlFile.path}`); 158 | }); 159 | }, Promise.resolve()); 160 | }) 161 | .then(() => { 162 | console.log('Upgrade success.'); 163 | }) 164 | .catch((e) => { 165 | console.error(e); 166 | }) 167 | .finally(() => { 168 | if (connection) connection.end(); 169 | }); 170 | } else { 171 | yargs.showHelp(); 172 | } 173 | -------------------------------------------------------------------------------- /src/models/apps.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | export interface AppsInterface extends Model { 5 | id: number; 6 | name: string; 7 | uid: number; 8 | os: number; 9 | platform: number; 10 | /** 11 | * @deprecated is_use_diff_text is no longer supported 12 | */ 13 | is_use_diff_text: number; 14 | created_at: Date; 15 | updated_at: Date; 16 | } 17 | 18 | export const Apps = sequelize.define( 19 | 'Apps', 20 | { 21 | id: { 22 | type: DataTypes.INTEGER({ length: 10 }), 23 | allowNull: false, 24 | autoIncrement: true, 25 | primaryKey: true, 26 | }, 27 | name: DataTypes.STRING, 28 | uid: DataTypes.BIGINT({ length: 20 }), 29 | os: DataTypes.INTEGER({ length: 3 }), 30 | platform: DataTypes.INTEGER({ length: 3 }), 31 | is_use_diff_text: DataTypes.INTEGER({ length: 3 }), 32 | created_at: DataTypes.DATE, 33 | updated_at: DataTypes.DATE, 34 | }, 35 | { 36 | tableName: 'apps', 37 | underscored: true, 38 | paranoid: true, 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/models/collaborators.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | export interface CollaboratorsInterface extends Model { 5 | id: number; 6 | appid: number; 7 | uid: number; 8 | roles: string; 9 | created_at: Date; 10 | updated_at: Date; 11 | } 12 | 13 | export const Collaborators = sequelize.define( 14 | 'Collaborators', 15 | { 16 | id: { 17 | type: DataTypes.BIGINT({ length: 20 }), 18 | allowNull: false, 19 | autoIncrement: true, 20 | primaryKey: true, 21 | }, 22 | appid: DataTypes.INTEGER({ length: 10 }), 23 | uid: DataTypes.BIGINT({ length: 20 }), 24 | roles: DataTypes.STRING, 25 | created_at: DataTypes.DATE, 26 | updated_at: DataTypes.DATE, 27 | }, 28 | { 29 | tableName: 'collaborators', 30 | underscored: true, 31 | paranoid: true, 32 | }, 33 | ); 34 | 35 | export async function findCollaboratorsByAppNameAndUid(uid: number, appName: string) { 36 | const sql = 37 | 'SELECT b.* FROM `apps` as a left join `collaborators` as b on (a.id = b.appid) where a.name= :appName and b.uid = :uid and a.`deleted_at` IS NULL and b.`deleted_at` IS NULL limit 0,1'; 38 | const data = await sequelize.query(sql, { 39 | replacements: { appName, uid }, 40 | model: Collaborators, 41 | }); 42 | return data.pop(); 43 | } 44 | -------------------------------------------------------------------------------- /src/models/deployments.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { DataTypes, Model } from 'sequelize'; 3 | import { AppError } from '../core/app-error'; 4 | import { sequelize } from '../core/utils/connections'; 5 | 6 | interface DeploymentsInterface extends Model { 7 | id: number; 8 | appid: number; 9 | name: string; 10 | description: string; 11 | deployment_key: string; 12 | last_deployment_version_id: number; 13 | label_id: number; 14 | created_at: Date; 15 | updated_at: Date; 16 | } 17 | 18 | export const Deployments = sequelize.define( 19 | 'Deployments', 20 | { 21 | id: { 22 | type: DataTypes.INTEGER({ length: 10 }), 23 | allowNull: false, 24 | autoIncrement: true, 25 | primaryKey: true, 26 | }, 27 | appid: DataTypes.INTEGER({ length: 10 }), 28 | name: DataTypes.STRING, 29 | description: DataTypes.STRING, 30 | deployment_key: DataTypes.STRING, 31 | last_deployment_version_id: DataTypes.INTEGER({ length: 10 }), 32 | label_id: DataTypes.INTEGER({ length: 10 }), 33 | created_at: DataTypes.DATE, 34 | updated_at: DataTypes.DATE, 35 | }, 36 | { 37 | tableName: 'deployments', 38 | underscored: true, 39 | paranoid: true, 40 | }, 41 | ); 42 | 43 | export function generateDeploymentsLabelId(deploymentId: number) { 44 | return sequelize.transaction((t) => { 45 | return Deployments.findByPk(deploymentId, { 46 | transaction: t, 47 | lock: t.LOCK.UPDATE, 48 | }).then((data) => { 49 | if (_.isEmpty(data)) { 50 | throw new AppError('does not find deployment'); 51 | } 52 | data.label_id += 1; 53 | return data.save({ transaction: t }).then((d) => { 54 | return d.label_id; 55 | }); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/models/deployments_history.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface DeploymentsHistoryInterface extends Model { 5 | id: number; 6 | deployment_id: number; 7 | package_id: number; 8 | created_at: Date; 9 | } 10 | 11 | export const DeploymentsHistory = sequelize.define( 12 | 'DeploymentsHistory', 13 | { 14 | id: { 15 | type: DataTypes.INTEGER({ length: 10 }), 16 | allowNull: false, 17 | autoIncrement: true, 18 | primaryKey: true, 19 | }, 20 | deployment_id: DataTypes.INTEGER({ length: 10 }), 21 | package_id: DataTypes.INTEGER({ length: 10 }), 22 | created_at: DataTypes.DATE, 23 | }, 24 | { 25 | tableName: 'deployments_history', 26 | underscored: true, 27 | updatedAt: false, 28 | paranoid: true, 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/models/deployments_versions.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | export interface DeploymentsVersionsInterface extends Model { 5 | id: number; 6 | deployment_id: number; 7 | app_version: string; 8 | current_package_id: number; 9 | min_version: number; 10 | max_version: number; 11 | created_at: Date; 12 | updated_at: Date; 13 | } 14 | 15 | export const DeploymentsVersions = sequelize.define( 16 | 'DeploymentsVersions', 17 | { 18 | id: { 19 | type: DataTypes.INTEGER({ length: 10 }), 20 | allowNull: false, 21 | autoIncrement: true, 22 | primaryKey: true, 23 | }, 24 | deployment_id: DataTypes.INTEGER({ length: 10 }), 25 | app_version: DataTypes.STRING, 26 | current_package_id: DataTypes.INTEGER({ length: 10 }), 27 | min_version: DataTypes.BIGINT({ length: 20 }), 28 | max_version: DataTypes.BIGINT({ length: 20 }), 29 | created_at: DataTypes.DATE, 30 | updated_at: DataTypes.DATE, 31 | }, 32 | { 33 | tableName: 'deployments_versions', 34 | underscored: true, 35 | paranoid: true, 36 | }, 37 | ); 38 | -------------------------------------------------------------------------------- /src/models/log_report_deploy.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface LogReportDeployInterface extends Model { 5 | id: number; 6 | status: number; 7 | package_id: number; 8 | client_unique_id: string; 9 | previous_label: string; 10 | previous_deployment_key: string; 11 | created_at: Date; 12 | } 13 | 14 | export const LogReportDeploy = sequelize.define( 15 | 'LogReportDeploy', 16 | { 17 | id: { 18 | type: DataTypes.BIGINT({ length: 20 }), 19 | allowNull: false, 20 | autoIncrement: true, 21 | primaryKey: true, 22 | }, 23 | status: DataTypes.INTEGER({ length: 3 }), 24 | package_id: DataTypes.INTEGER({ length: 10 }), 25 | client_unique_id: DataTypes.STRING, 26 | previous_label: DataTypes.STRING, 27 | previous_deployment_key: DataTypes.STRING, 28 | created_at: DataTypes.DATE, 29 | }, 30 | { 31 | tableName: 'log_report_deploy', 32 | underscored: true, 33 | updatedAt: false, 34 | paranoid: true, 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /src/models/log_report_download.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface LogReportDownloadInterface extends Model { 5 | id: number; 6 | package_id: number; 7 | client_unique_id: string; 8 | created_at: Date; 9 | } 10 | 11 | export const LogReportDownload = sequelize.define( 12 | 'LogReportDownload', 13 | { 14 | id: { 15 | type: DataTypes.BIGINT({ length: 20 }), 16 | allowNull: false, 17 | autoIncrement: true, 18 | primaryKey: true, 19 | }, 20 | package_id: DataTypes.INTEGER({ length: 10 }), 21 | client_unique_id: DataTypes.STRING, 22 | created_at: DataTypes.DATE, 23 | }, 24 | { 25 | tableName: 'log_report_download', 26 | underscored: true, 27 | updatedAt: false, 28 | paranoid: true, 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/models/packages.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | export interface PackagesInterface extends Model { 5 | id: number; 6 | deployment_version_id: number; 7 | deployment_id: number; 8 | description: string; 9 | package_hash: string; 10 | blob_url: string; 11 | size: number; 12 | manifest_blob_url: string; 13 | release_method: string; 14 | label: string; 15 | original_label: string; 16 | original_deployment: string; 17 | released_by: number; 18 | is_mandatory: number; 19 | is_disabled: number; 20 | rollout: number; 21 | created_at: Date; 22 | updated_at: Date; 23 | } 24 | 25 | export const Packages = sequelize.define( 26 | 'Packages', 27 | { 28 | id: { 29 | type: DataTypes.INTEGER({ length: 10 }), 30 | allowNull: false, 31 | autoIncrement: true, 32 | primaryKey: true, 33 | }, 34 | deployment_version_id: DataTypes.INTEGER({ length: 10 }), 35 | deployment_id: DataTypes.INTEGER({ length: 10 }), 36 | description: DataTypes.STRING, 37 | package_hash: DataTypes.STRING, 38 | blob_url: DataTypes.STRING, 39 | size: DataTypes.INTEGER({ length: 10 }), 40 | manifest_blob_url: DataTypes.STRING, 41 | release_method: DataTypes.STRING, 42 | label: DataTypes.STRING, 43 | original_label: DataTypes.STRING, 44 | original_deployment: DataTypes.STRING, 45 | released_by: DataTypes.BIGINT({ length: 20 }), 46 | is_mandatory: DataTypes.INTEGER({ length: 3 }), 47 | is_disabled: DataTypes.INTEGER({ length: 3 }), 48 | rollout: DataTypes.INTEGER({ length: 3 }), 49 | created_at: DataTypes.DATE, 50 | updated_at: DataTypes.DATE, 51 | }, 52 | { 53 | tableName: 'packages', 54 | underscored: true, 55 | paranoid: true, 56 | }, 57 | ); 58 | -------------------------------------------------------------------------------- /src/models/packages_diff.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface PackagesDiffInterface extends Model { 5 | id: number; 6 | package_id: number; 7 | diff_against_package_hash: string; 8 | diff_blob_url: string; 9 | diff_size: number; 10 | created_at: Date; 11 | updated_at: Date; 12 | } 13 | 14 | export const PackagesDiff = sequelize.define( 15 | 'PackagesDiff', 16 | { 17 | id: { 18 | type: DataTypes.INTEGER({ length: 10 }), 19 | allowNull: false, 20 | autoIncrement: true, 21 | primaryKey: true, 22 | }, 23 | package_id: DataTypes.INTEGER({ length: 10 }), 24 | diff_against_package_hash: DataTypes.STRING, 25 | diff_blob_url: DataTypes.STRING, 26 | diff_size: DataTypes.INTEGER({ length: 10 }), 27 | created_at: DataTypes.DATE, 28 | updated_at: DataTypes.DATE, 29 | }, 30 | { 31 | tableName: 'packages_diff', 32 | underscored: true, 33 | paranoid: true, 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /src/models/packages_metrics.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface PackagesMetricsInterface extends Model { 5 | id: number; 6 | package_id: number; 7 | active: number; 8 | downloaded: number; 9 | failed: number; 10 | installed: number; 11 | created_at: Date; 12 | updated_at: Date; 13 | } 14 | 15 | export const PackagesMetrics = sequelize.define( 16 | 'PackagesMetrics', 17 | { 18 | id: { 19 | type: DataTypes.INTEGER({ length: 10 }), 20 | allowNull: false, 21 | autoIncrement: true, 22 | primaryKey: true, 23 | }, 24 | package_id: DataTypes.INTEGER({ length: 10 }), 25 | active: DataTypes.INTEGER({ length: 10 }), 26 | downloaded: DataTypes.INTEGER({ length: 10 }), 27 | failed: DataTypes.INTEGER({ length: 10 }), 28 | installed: DataTypes.INTEGER({ length: 10 }), 29 | created_at: DataTypes.DATE, 30 | updated_at: DataTypes.DATE, 31 | }, 32 | { 33 | tableName: 'packages_metrics', 34 | underscored: true, 35 | paranoid: true, 36 | }, 37 | ); 38 | -------------------------------------------------------------------------------- /src/models/user_tokens.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface UserTokensInterface extends Model { 5 | id: number; 6 | uid: number; 7 | name: string; 8 | tokens: string; 9 | description: string; 10 | is_session: number; 11 | created_by: string; 12 | created_at: Date; 13 | expires_at: Date; 14 | } 15 | 16 | export const UserTokens = sequelize.define( 17 | 'UserTokens', 18 | { 19 | id: { 20 | type: DataTypes.BIGINT({ length: 20 }), 21 | allowNull: false, 22 | autoIncrement: true, 23 | primaryKey: true, 24 | }, 25 | uid: DataTypes.BIGINT({ length: 20 }), 26 | name: DataTypes.STRING, 27 | tokens: DataTypes.STRING, 28 | description: DataTypes.STRING, 29 | is_session: DataTypes.INTEGER({ length: 3 }), 30 | created_by: DataTypes.STRING, 31 | created_at: DataTypes.DATE, 32 | expires_at: DataTypes.DATE, 33 | }, 34 | { 35 | updatedAt: false, 36 | tableName: 'user_tokens', 37 | underscored: true, 38 | paranoid: true, 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/models/users.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | export interface UsersInterface extends Model { 5 | id: number; 6 | username: string; 7 | password: string; 8 | email: string; 9 | identical: string; 10 | ack_code: string; 11 | created_at: Date; 12 | updated_at: Date; 13 | } 14 | 15 | export const Users = sequelize.define( 16 | 'Users', 17 | { 18 | id: { 19 | type: DataTypes.BIGINT({ length: 20 }), 20 | allowNull: false, 21 | autoIncrement: true, 22 | primaryKey: true, 23 | }, 24 | username: DataTypes.STRING, 25 | password: DataTypes.STRING, 26 | email: DataTypes.STRING, 27 | identical: DataTypes.STRING, 28 | ack_code: DataTypes.STRING, 29 | created_at: DataTypes.DATE, 30 | updated_at: DataTypes.DATE, 31 | }, 32 | { 33 | tableName: 'users', 34 | underscored: true, 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /src/models/versions.ts: -------------------------------------------------------------------------------- 1 | import { DataTypes, Model } from 'sequelize'; 2 | import { sequelize } from '../core/utils/connections'; 3 | 4 | interface VersionsInterface extends Model { 5 | id: number; 6 | type: number; 7 | version: string; 8 | } 9 | 10 | export const Versions = sequelize.define( 11 | 'Versions', 12 | { 13 | id: { 14 | type: DataTypes.INTEGER({ length: 10 }), 15 | allowNull: false, 16 | autoIncrement: true, 17 | primaryKey: true, 18 | }, 19 | type: DataTypes.INTEGER, 20 | version: DataTypes.STRING, 21 | }, 22 | { 23 | tableName: 'versions', 24 | updatedAt: false, 25 | createdAt: false, 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /src/routes/accessKeys.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import _ from 'lodash'; 3 | import moment from 'moment'; 4 | import { AppError } from '../core/app-error'; 5 | import { checkToken, Req } from '../core/middleware'; 6 | import { accountManager } from '../core/services/account-manager'; 7 | import { randToken } from '../core/utils/security'; 8 | import { UserTokens } from '../models/user_tokens'; 9 | 10 | export const accessKeysRouter = express.Router(); 11 | 12 | accessKeysRouter.get('/', checkToken, (req: Req, res, next) => { 13 | const { logger } = req; 14 | const uid = req.users.id; 15 | logger.info('try get acceesKeys', { uid }); 16 | accountManager 17 | .getAllAccessKeyByUid(uid) 18 | .then((accessKeys) => { 19 | logger.info('get acceesKeys success', { uid }); 20 | res.send({ accessKeys }); 21 | }) 22 | .catch((e) => { 23 | next(e); 24 | }); 25 | }); 26 | 27 | accessKeysRouter.post( 28 | '/', 29 | checkToken, 30 | ( 31 | req: Req< 32 | Record, 33 | { 34 | createdBy: string; 35 | friendlyName: string; 36 | ttl: string; 37 | description: string; 38 | } 39 | >, 40 | res, 41 | next, 42 | ) => { 43 | const { logger, body } = req; 44 | const uid = req.users.id; 45 | const createdBy = _.trim(body.createdBy); 46 | const friendlyName = _.trim(body.friendlyName); 47 | const ttl = parseInt(body.ttl, 10); 48 | const description = _.trim(body.description); 49 | logger.info('try to generate access key', { 50 | uid, 51 | name: friendlyName, 52 | body: JSON.stringify(body), 53 | }); 54 | return accountManager 55 | .isExsitAccessKeyName(uid, friendlyName) 56 | .then((data) => { 57 | if (!_.isEmpty(data)) { 58 | throw new AppError(`The access key "${friendlyName}" already exists.`); 59 | } 60 | }) 61 | .then(() => { 62 | const { identical } = req.users; 63 | const newAccessKey = randToken(28).concat(identical); 64 | return accountManager.createAccessKey( 65 | uid, 66 | newAccessKey, 67 | ttl, 68 | friendlyName, 69 | createdBy, 70 | description, 71 | ); 72 | }) 73 | .then((newToken) => { 74 | const info = { 75 | name: newToken.tokens, 76 | createdTime: moment(newToken.created_at).valueOf(), 77 | createdBy: newToken.created_by, 78 | expires: moment(newToken.expires_at).valueOf(), 79 | description: newToken.description, 80 | friendlyName: newToken.name, 81 | }; 82 | logger.info('create access key success', { 83 | uid, 84 | name: newToken.name, 85 | }); 86 | res.send({ accessKey: info }); 87 | }) 88 | .catch((e) => { 89 | if (e instanceof AppError) { 90 | logger.info('create access key failed', { 91 | uid, 92 | name: friendlyName, 93 | error: e.message, 94 | }); 95 | 96 | res.status(406).send(e.message); 97 | } else { 98 | next(e); 99 | } 100 | }); 101 | }, 102 | ); 103 | 104 | accessKeysRouter.delete('/:name', checkToken, (req: Req<{ name: string }>, res, next) => { 105 | const { logger, params } = req; 106 | const name = _.trim(params.name); 107 | const uid = req.users.id; 108 | logger.info('try to delete access key', { uid, name }); 109 | return UserTokens.destroy({ where: { name, uid } }) 110 | .then(() => { 111 | logger.info('delete acceesKey success', { uid, name }); 112 | res.send({ friendlyName: name }); 113 | }) 114 | .catch((e) => { 115 | if (e instanceof AppError) { 116 | logger.info('delete acceesKey failed', { uid, name }); 117 | res.status(406).send(e.message); 118 | } else { 119 | next(e); 120 | } 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/routes/account.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { checkToken, Req } from '../core/middleware'; 3 | 4 | export const accountRouter = express.Router(); 5 | 6 | accountRouter.get('/', checkToken, (req: Req, res) => { 7 | const { logger } = req; 8 | const account = { 9 | email: req.users.email, 10 | linkedProviders: [], 11 | name: req.users.username, 12 | }; 13 | logger.info('check account info', { account: JSON.stringify(account) }); 14 | res.send({ account }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import _ from 'lodash'; 4 | import { AppError } from '../core/app-error'; 5 | import { config } from '../core/config'; 6 | import { Req } from '../core/middleware'; 7 | import { accountManager } from '../core/services/account-manager'; 8 | import { md5 } from '../core/utils/security'; 9 | 10 | // route for auth web pages 11 | export const authRouter = express.Router(); 12 | 13 | authRouter.get('/password', (req: Req, res) => { 14 | res.render('auth/password', { title: 'CodePushServer' }); 15 | }); 16 | 17 | authRouter.get('/login', (req: Req, res) => { 18 | res.render('auth/login', { 19 | title: 'CodePushServer', 20 | email: req.query.email || '', 21 | showRegister: config.common.allowRegistration, 22 | }); 23 | }); 24 | 25 | authRouter.get('/link', (req: Req, res) => { 26 | res.redirect(`/auth/login`); 27 | }); 28 | 29 | authRouter.get('/register', (req: Req, res) => { 30 | if (config.common.allowRegistration) { 31 | res.render('auth/register', { title: 'CodePushServer', email: req.query.email || '' }); 32 | } else { 33 | res.redirect(`/auth/login`); 34 | } 35 | }); 36 | 37 | authRouter.get('/confirm', (req: Req, res) => { 38 | res.render('auth/confirm', { title: 'CodePushServer', email: req.query.email || '' }); 39 | }); 40 | 41 | authRouter.post('/logout', (req: Req, res) => { 42 | res.send('ok'); 43 | }); 44 | 45 | authRouter.post( 46 | '/login', 47 | (req: Req, res, next) => { 48 | const { logger, body } = req; 49 | const account = _.trim(body.account); 50 | const password = _.trim(body.password); 51 | logger.info('try login', { 52 | account, 53 | }); 54 | accountManager 55 | .login(account, password) 56 | .then((users) => { 57 | logger.info('login success', { 58 | account, 59 | uid: users.id, 60 | }); 61 | return jwt.sign( 62 | { uid: users.id, hash: md5(users.ack_code), expiredIn: 7200 }, 63 | config.jwt.tokenSecret, 64 | ); 65 | }) 66 | .then((token) => { 67 | logger.info('jwt token signed', { 68 | account, 69 | }); 70 | res.send({ status: 'OK', results: { tokens: token } }); 71 | }) 72 | .catch((e) => { 73 | if (e instanceof AppError) { 74 | logger.info('login failed', { 75 | account, 76 | error: e.message, 77 | }); 78 | res.send({ status: 'ERROR', message: e.message }); 79 | } else { 80 | next(e); 81 | } 82 | }); 83 | }, 84 | ); 85 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { AppError } from '../core/app-error'; 3 | import { i18n } from '../core/i18n'; 4 | import { checkToken, Req } from '../core/middleware'; 5 | import { clientManager } from '../core/services/client-manager'; 6 | 7 | export const indexRouter = express.Router(); 8 | 9 | indexRouter.get('/', (req, res) => { 10 | res.render('index', { title: 'CodePushServer' }); 11 | }); 12 | 13 | indexRouter.get('/tokens', (req, res) => { 14 | // eslint-disable-next-line no-underscore-dangle 15 | res.render('tokens', { title: `${i18n.__('Obtain')} token` }); 16 | }); 17 | 18 | indexRouter.get( 19 | '/updateCheck', 20 | ( 21 | req: Req< 22 | void, 23 | void, 24 | { 25 | deploymentKey: string; 26 | appVersion: string; 27 | label: string; 28 | packageHash: string; 29 | clientUniqueId: string; 30 | } 31 | >, 32 | res, 33 | next, 34 | ) => { 35 | const { logger, query } = req; 36 | logger.info('updateCheck', { 37 | query: JSON.stringify(query), 38 | }); 39 | const { deploymentKey, appVersion, label, packageHash, clientUniqueId } = query; 40 | clientManager 41 | .updateCheckFromCache( 42 | deploymentKey, 43 | appVersion, 44 | label, 45 | packageHash, 46 | clientUniqueId, 47 | logger, 48 | ) 49 | .then((rs) => { 50 | // 灰度检测 51 | return clientManager 52 | .chosenMan(rs.packageId, rs.rollout, clientUniqueId) 53 | .then((data) => { 54 | if (!data) { 55 | rs.isAvailable = false; 56 | return rs; 57 | } 58 | return rs; 59 | }); 60 | }) 61 | .then((rs) => { 62 | logger.info('updateCheck success'); 63 | 64 | delete rs.packageId; 65 | delete rs.rollout; 66 | res.send({ updateInfo: rs }); 67 | }) 68 | .catch((e) => { 69 | if (e instanceof AppError) { 70 | logger.info('updateCheck failed', { 71 | error: e.message, 72 | }); 73 | res.status(404).send(e.message); 74 | } else { 75 | next(e); 76 | } 77 | }); 78 | }, 79 | ); 80 | 81 | indexRouter.post( 82 | '/reportStatus/download', 83 | ( 84 | req: Req< 85 | void, 86 | { 87 | clientUniqueId: string; 88 | label: string; 89 | deploymentKey: string; 90 | }, 91 | void 92 | >, 93 | res, 94 | ) => { 95 | const { logger, body } = req; 96 | logger.info('reportStatus/download', { 97 | body: JSON.stringify(body), 98 | }); 99 | const { clientUniqueId, label, deploymentKey } = body; 100 | clientManager.reportStatusDownload(deploymentKey, label, clientUniqueId).catch((err) => { 101 | if (err instanceof AppError) { 102 | logger.info('reportStatus/deploy failed', { 103 | error: err.message, 104 | }); 105 | } else { 106 | logger.error(err); 107 | } 108 | }); 109 | res.send('OK'); 110 | }, 111 | ); 112 | 113 | indexRouter.post( 114 | '/reportStatus/deploy', 115 | ( 116 | req: Req< 117 | void, 118 | { 119 | clientUniqueId: string; 120 | label: string; 121 | deploymentKey: string; 122 | }, 123 | void 124 | >, 125 | res, 126 | ) => { 127 | const { logger, body } = req; 128 | logger.info('reportStatus/deploy', { 129 | body: JSON.stringify(body), 130 | }); 131 | const { clientUniqueId, label, deploymentKey } = body; 132 | clientManager 133 | .reportStatusDeploy(deploymentKey, label, clientUniqueId, req.body) 134 | .catch((err) => { 135 | if (err instanceof AppError) { 136 | logger.info('reportStatus/deploy failed', { 137 | error: err.message, 138 | }); 139 | } else { 140 | logger.error(err); 141 | } 142 | }); 143 | res.send('OK'); 144 | }, 145 | ); 146 | 147 | indexRouter.get('/authenticated', checkToken, (req, res) => { 148 | return res.send({ authenticated: true }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/routes/indexV1.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { AppError } from '../core/app-error'; 3 | import { Req } from '../core/middleware'; 4 | import { clientManager } from '../core/services/client-manager'; 5 | 6 | // routes for latest code push client 7 | export const indexV1Router = express.Router(); 8 | 9 | indexV1Router.get( 10 | '/update_check', 11 | ( 12 | req: Req< 13 | void, 14 | void, 15 | { 16 | deployment_key: string; 17 | app_version: string; 18 | label: string; 19 | package_hash: string; 20 | is_companion: unknown; 21 | client_unique_id: string; 22 | } 23 | >, 24 | res, 25 | next, 26 | ) => { 27 | const { logger, query } = req; 28 | logger.info('try update_check', { 29 | query: JSON.stringify(query), 30 | }); 31 | const { 32 | deployment_key: deploymentKey, 33 | app_version: appVersion, 34 | label, 35 | package_hash: packageHash, 36 | client_unique_id: clientUniqueId, 37 | } = query; 38 | clientManager 39 | .updateCheckFromCache( 40 | deploymentKey, 41 | appVersion, 42 | label, 43 | packageHash, 44 | clientUniqueId, 45 | logger, 46 | ) 47 | .then((rs) => { 48 | // 灰度检测 49 | return clientManager 50 | .chosenMan(rs.packageId, rs.rollout, clientUniqueId) 51 | .then((data) => { 52 | if (!data) { 53 | rs.isAvailable = false; 54 | return rs; 55 | } 56 | return rs; 57 | }); 58 | }) 59 | .then((rs) => { 60 | logger.info('update_check success'); 61 | 62 | res.send({ 63 | update_info: { 64 | download_url: rs.downloadUrl, 65 | description: rs.description, 66 | is_available: rs.isAvailable, 67 | is_disabled: rs.isDisabled, 68 | // Note: need to use appVersion here to get it compatible with client side change... 69 | // https://github.com/microsoft/code-push/commit/7d2ffff395cc54db98aefba7c67889f509e8c249#diff-a937c637a47cbd31cbb52c89bef7d197R138 70 | target_binary_range: rs.appVersion, 71 | label: rs.label, 72 | package_hash: rs.packageHash, 73 | package_size: rs.packageSize, 74 | should_run_binary_version: rs.shouldRunBinaryVersion, 75 | update_app_version: rs.updateAppVersion, 76 | is_mandatory: rs.isMandatory, 77 | }, 78 | }); 79 | }) 80 | .catch((e) => { 81 | if (e instanceof AppError) { 82 | logger.info('update check failed', { 83 | error: e.message, 84 | }); 85 | res.status(404).send(e.message); 86 | } else { 87 | next(e); 88 | } 89 | }); 90 | }, 91 | ); 92 | 93 | indexV1Router.post( 94 | '/report_status/download', 95 | ( 96 | req: Req< 97 | void, 98 | { 99 | client_unique_id: string; 100 | label: string; 101 | deployment_key: string; 102 | }, 103 | void 104 | >, 105 | res, 106 | ) => { 107 | const { logger, body } = req; 108 | logger.info('report_status/download', { body: JSON.stringify(body) }); 109 | const { client_unique_id: clientUniqueId, label, deployment_key: deploymentKey } = body; 110 | clientManager.reportStatusDownload(deploymentKey, label, clientUniqueId).catch((err) => { 111 | if (err instanceof AppError) { 112 | logger.info('report_status/download failed', { 113 | error: err.message, 114 | }); 115 | } else { 116 | logger.error(err); 117 | } 118 | }); 119 | res.send('OK'); 120 | }, 121 | ); 122 | 123 | indexV1Router.post( 124 | '/report_status/deploy', 125 | ( 126 | req: Req< 127 | void, 128 | { 129 | client_unique_id: string; 130 | label: string; 131 | deployment_key: string; 132 | }, 133 | void 134 | >, 135 | res, 136 | ) => { 137 | const { logger, body } = req; 138 | logger.info('report_status/deploy', { body: JSON.stringify(body) }); 139 | const { client_unique_id: clientUniqueId, label, deployment_key: deploymentKey } = body; 140 | clientManager 141 | .reportStatusDeploy(deploymentKey, label, clientUniqueId, req.body) 142 | .catch((err) => { 143 | if (err instanceof AppError) { 144 | logger.info('report_status/deploy failed', { 145 | error: err.message, 146 | }); 147 | } else { 148 | logger.error(err); 149 | } 150 | }); 151 | res.send('OK'); 152 | }, 153 | ); 154 | -------------------------------------------------------------------------------- /src/routes/users.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import _ from 'lodash'; 3 | import { AppError } from '../core/app-error'; 4 | import { checkToken, Req } from '../core/middleware'; 5 | import { accountManager } from '../core/services/account-manager'; 6 | import { Users } from '../models/users'; 7 | 8 | export const usersRouter = express.Router(); 9 | 10 | usersRouter.get('/', checkToken, (req: Req, res) => { 11 | res.send({ title: 'CodePushServer' }); 12 | }); 13 | 14 | usersRouter.post( 15 | '/', 16 | ( 17 | req: Req< 18 | void, 19 | { 20 | email: string; 21 | token: string; 22 | password: string; 23 | }, 24 | void 25 | >, 26 | res, 27 | next, 28 | ) => { 29 | const { logger, body } = req; 30 | const email = _.trim(body.email); 31 | const token = _.trim(body.token); 32 | const password = _.trim(body.password); 33 | logger.info('try register account', { email, token }); 34 | return accountManager 35 | .checkRegisterCode(email, token) 36 | .then(() => { 37 | if (_.isString(password) && password.length < 6) { 38 | throw new AppError('请您输入6~20位长度的密码'); 39 | } 40 | return accountManager.register(email, password); 41 | }) 42 | .then(() => { 43 | logger.info('register account success', { email }); 44 | res.send({ status: 'OK' }); 45 | }) 46 | .catch((e) => { 47 | if (e instanceof AppError) { 48 | logger.info('register account failed', { email, error: e.message }); 49 | res.send({ status: 'ERROR', message: e.message }); 50 | } else { 51 | next(e); 52 | } 53 | }); 54 | }, 55 | ); 56 | 57 | usersRouter.get('/exists', (req: Req, res, next) => { 58 | const email = _.trim(req.query.email); 59 | if (!email) { 60 | res.send({ status: 'ERROR', message: '请您输入邮箱地址' }); 61 | return; 62 | } 63 | Users.findOne({ where: { email } }) 64 | .then((u) => { 65 | res.send({ status: 'OK', exists: !!u }); 66 | }) 67 | .catch((e) => { 68 | if (e instanceof AppError) { 69 | res.send({ status: 'ERROR', message: e.message }); 70 | } else { 71 | next(e); 72 | } 73 | }); 74 | }); 75 | 76 | usersRouter.post('/registerCode', (req: Req, res, next) => { 77 | const { logger, body } = req; 78 | const { email } = body; 79 | logger.info('try send register code', { email }); 80 | return accountManager 81 | .sendRegisterCode(email) 82 | .then(() => { 83 | logger.info('send register code success', { email }); 84 | res.send({ status: 'OK' }); 85 | }) 86 | .catch((e) => { 87 | if (e instanceof AppError) { 88 | logger.info('send register code error', { email, error: e.message }); 89 | res.send({ status: 'ERROR', message: e.message }); 90 | } else { 91 | next(e); 92 | } 93 | }); 94 | }); 95 | 96 | usersRouter.get( 97 | '/registerCode/exists', 98 | (req: Req, res, next) => { 99 | const { query } = req; 100 | const email = _.trim(query.email); 101 | const token = _.trim(query.token); 102 | return accountManager 103 | .checkRegisterCode(email, token) 104 | .then(() => { 105 | res.send({ status: 'OK' }); 106 | }) 107 | .catch((e) => { 108 | if (e instanceof AppError) { 109 | res.send({ status: 'ERROR', message: e.message }); 110 | } else { 111 | next(e); 112 | } 113 | }); 114 | }, 115 | ); 116 | 117 | // 修改密码 118 | usersRouter.patch( 119 | '/password', 120 | checkToken, 121 | ( 122 | req: Req< 123 | Record, 124 | { 125 | oldPassword: string; 126 | newPassword: string; 127 | } 128 | >, 129 | res, 130 | next, 131 | ) => { 132 | const { logger, body } = req; 133 | const oldPassword = _.trim(body.oldPassword); 134 | const newPassword = _.trim(body.newPassword); 135 | const uid = req.users.id; 136 | logger.info('try change password', { uid }); 137 | return accountManager 138 | .changePassword(uid, oldPassword, newPassword) 139 | .then(() => { 140 | logger.info('change password success', { uid }); 141 | res.send({ status: 'OK' }); 142 | }) 143 | .catch((e) => { 144 | if (e instanceof AppError) { 145 | logger.info('change password failed', { uid }); 146 | res.send({ status: 'ERROR', message: e.message }); 147 | } else { 148 | next(e); 149 | } 150 | }); 151 | }, 152 | ); 153 | -------------------------------------------------------------------------------- /src/www.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import http from 'http'; 4 | import { logger } from 'kv-logger'; 5 | import _ from 'lodash'; 6 | import validator from 'validator'; 7 | import { app } from './app'; 8 | import { CURRENT_DB_VERSION } from './core/const'; 9 | import { Versions } from './models/versions'; 10 | 11 | /** 12 | * Normalize a port into a number, string, or false. 13 | */ 14 | function normalizePort(val): number | string | false { 15 | const port = parseInt(val, 10); 16 | 17 | if (Number.isNaN(port)) { 18 | // named pipe 19 | return val; 20 | } 21 | 22 | if (port >= 0) { 23 | // port number 24 | return port; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | // check if the db is initialized 31 | Versions.findOne({ where: { type: 1 } }) 32 | .then((v) => { 33 | if (!v || v.version !== CURRENT_DB_VERSION) { 34 | throw new Error( 35 | 'Please upgrade your database. use `npm run upgrade` or `code-push-server-db upgrade`', 36 | ); 37 | } 38 | // create server and listen 39 | const server = http.createServer(app); 40 | 41 | const port = normalizePort(process.env.PORT || '3000'); 42 | 43 | let host = null; 44 | if (process.env.HOST) { 45 | logger.debug(`process.env.HOST ${process.env.HOST}`); 46 | if (validator.isIP(process.env.HOST)) { 47 | logger.debug(`${process.env.HOST} valid`); 48 | host = process.env.HOST; 49 | } else { 50 | logger.warn(`process.env.HOST ${process.env.HOST} is invalid, use 0.0.0.0 instead`); 51 | } 52 | } 53 | 54 | server.listen(port, host); 55 | 56 | server.on('error', (error: Error & { syscall: string; code: string }) => { 57 | if (error.syscall === 'listen') { 58 | const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`; 59 | 60 | // handle specific listen errors with friendly messages 61 | switch (error.code) { 62 | case 'EACCES': 63 | logger.error(`${bind} requires elevated privileges`); 64 | process.exit(1); 65 | break; 66 | case 'EADDRINUSE': 67 | logger.error(`${bind} is already in use`); 68 | process.exit(1); 69 | break; 70 | default: 71 | break; 72 | } 73 | } 74 | logger.error(error); 75 | throw error; 76 | }); 77 | 78 | server.on('listening', () => { 79 | const addr = server.address(); 80 | logger.info(`server is listening on ${JSON.stringify(addr)}`); 81 | }); 82 | }) 83 | .catch((e) => { 84 | if (_.startsWith(e.message, 'ER_NO_SUCH_TABLE')) { 85 | logger.error( 86 | new Error( 87 | 'Please upgrade your database. use `npm run upgrade` or `code-push-server-db upgrade`', 88 | ), 89 | ); 90 | } else { 91 | logger.error(e); 92 | } 93 | process.exit(1); 94 | }); 95 | -------------------------------------------------------------------------------- /tests/api/accessKeys/accessKeys.test.js: -------------------------------------------------------------------------------- 1 | const { app } = require('../../../bin/app'); 2 | const request = require('supertest')(app); 3 | const should = require('should'); 4 | const _ = require('lodash'); 5 | 6 | describe('api/accessKeys/accessKeys.test.js', function () { 7 | var account = '522539441@qq.com'; 8 | var password = '123456'; 9 | var authToken; 10 | var friendlyName = 'test'; 11 | var newFriendlyName = 'newtest'; 12 | before(function (done) { 13 | request 14 | .post('/auth/login') 15 | .send({ 16 | account: account, 17 | password: password, 18 | }) 19 | .end(function (err, res) { 20 | should.not.exist(err); 21 | var rs = JSON.parse(res.text); 22 | rs.should.containEql({ status: 'OK' }); 23 | authToken = Buffer.from(`auth:${_.get(rs, 'results.tokens')}`).toString('base64'); 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('create accessKeys', function (done) { 29 | it('should create accessKeys successful', function (done) { 30 | request 31 | .post(`/accessKeys`) 32 | .set('Authorization', `Basic ${authToken}`) 33 | .send({ 34 | createdBy: 'tablee', 35 | friendlyName: friendlyName, 36 | ttl: 30 * 24 * 60 * 60, 37 | }) 38 | .end(function (err, res) { 39 | should.not.exist(err); 40 | res.status.should.equal(200); 41 | var rs = JSON.parse(res.text); 42 | rs.should.have.properties('accessKey'); 43 | rs.accessKey.should.have.properties([ 44 | 'name', 45 | 'createdTime', 46 | 'createdBy', 47 | 'expires', 48 | 'description', 49 | 'friendlyName', 50 | ]); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should not create accessKeys successful when friendlyName exist', function (done) { 56 | request 57 | .post(`/accessKeys`) 58 | .set('Authorization', `Basic ${authToken}`) 59 | .send({ 60 | createdBy: 'tablee', 61 | friendlyName: friendlyName, 62 | ttl: 30 * 24 * 60 * 60, 63 | }) 64 | .end(function (err, res) { 65 | should.not.exist(err); 66 | res.status.should.equal(406); 67 | res.text.should.equal(`The access key "${friendlyName}" already exists.`); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('list accessKeys', function (done) { 74 | it('should list accessKeys successful', function (done) { 75 | request 76 | .get(`/accessKeys`) 77 | .set('Authorization', `Basic ${authToken}`) 78 | .send() 79 | .end(function (err, res) { 80 | should.not.exist(err); 81 | res.status.should.equal(200); 82 | var rs = JSON.parse(res.text); 83 | rs.should.have.properties('accessKeys'); 84 | rs.accessKeys.should.be.an.instanceOf(Array); 85 | rs.accessKeys.should.matchEach(function (it) { 86 | return it.should.have.properties([ 87 | 'name', 88 | 'createdTime', 89 | 'createdBy', 90 | 'expires', 91 | 'description', 92 | 'friendlyName', 93 | ]); 94 | }); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('delete accessKeys', function (done) { 101 | it('should delete accessKeys successful', function (done) { 102 | request 103 | .delete(`/accessKeys/${encodeURI(newFriendlyName)}`) 104 | .set('Authorization', `Basic ${authToken}`) 105 | .send() 106 | .end(function (err, res) { 107 | should.not.exist(err); 108 | res.status.should.equal(200); 109 | var rs = JSON.parse(res.text); 110 | rs.should.have.properties('friendlyName'); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /tests/api/account/account.test.js: -------------------------------------------------------------------------------- 1 | const { app } = require('../../../bin/app'); 2 | const request = require('supertest')(app); 3 | const should = require('should'); 4 | const _ = require('lodash'); 5 | 6 | describe('api/account/account.test.js', function () { 7 | var account = '522539441@qq.com'; 8 | var password = '123456'; 9 | 10 | describe('user modules', function (done) { 11 | var authToken; 12 | before(function (done) { 13 | request 14 | .post('/auth/login') 15 | .send({ 16 | account: account, 17 | password: password, 18 | }) 19 | .end(function (err, res) { 20 | should.not.exist(err); 21 | var rs = JSON.parse(res.text); 22 | rs.should.containEql({ status: 'OK' }); 23 | authToken = Buffer.from(`auth:${_.get(rs, 'results.tokens')}`).toString( 24 | 'base64', 25 | ); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should get account info successful', function (done) { 31 | request 32 | .get(`/account`) 33 | .set('Authorization', `Basic ${authToken}`) 34 | .send() 35 | .end(function (err, res) { 36 | should.not.exist(err); 37 | res.status.should.equal(200); 38 | var rs = JSON.parse(res.text); 39 | rs.should.have.properties('account'); 40 | rs.account.should.have.properties(['email', 'linkedProviders', 'name']); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/api/apps/bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/tests/api/apps/bundle.zip -------------------------------------------------------------------------------- /tests/api/apps/bundle_v2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/tests/api/apps/bundle_v2.zip -------------------------------------------------------------------------------- /tests/api/auth/auth.test.js: -------------------------------------------------------------------------------- 1 | const { app } = require('../../../bin/app'); 2 | const request = require('supertest')(app); 3 | const should = require('should'); 4 | const _ = require('lodash'); 5 | 6 | const { config } = require('../../../bin/core/config'); 7 | 8 | describe('api/auth/test.js', function () { 9 | var account = 'user@domain.tld'; 10 | var password = '123456'; 11 | 12 | describe('sign in view', function (done) { 13 | it('should show sign in view successful', function (done) { 14 | request 15 | .get('/auth/login') 16 | .send() 17 | .end(function (err, res) { 18 | should.not.exist(err); 19 | res.status.should.equal(200); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('sign up view', function (done) { 26 | it('should show sign in redirect view if sign up not enabled', function (done) { 27 | _.set(config, 'common.allowRegistration', false); 28 | request 29 | .get('/auth/register') 30 | .send() 31 | .end(function (err, res) { 32 | should.not.exist(err); 33 | res.status.should.equal(302); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should show sign up view successful', function (done) { 39 | _.set(config, 'common.allowRegistration', true); 40 | request 41 | .get('/auth/register') 42 | .send() 43 | .end(function (err, res) { 44 | should.not.exist(err); 45 | res.status.should.equal(200); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('sign in', function (done) { 52 | it('should not sign in successful when account is empty', function (done) { 53 | request 54 | .post('/auth/login') 55 | .send({ 56 | account: '', 57 | password: password, 58 | }) 59 | .end(function (err, res) { 60 | should.not.exist(err); 61 | JSON.parse(res.text).should.containEql({ 62 | status: 'ERROR', 63 | errorMessage: 'Please enter your email address', 64 | }); 65 | done(); 66 | }); 67 | }); 68 | it('should not sign in successful when account is not exist', function (done) { 69 | request 70 | .post('/auth/login') 71 | .send({ 72 | account: account + '1', 73 | password: password, 74 | }) 75 | .end(function (err, res) { 76 | should.not.exist(err); 77 | JSON.parse(res.text).should.containEql({ 78 | status: 'ERROR', 79 | errorMessage: 'The email or password you entered is incorrect', 80 | }); 81 | done(); 82 | }); 83 | }); 84 | it('should not sign in successful when password is wrong', function (done) { 85 | request 86 | .post('/auth/login') 87 | .send({ 88 | account: account, 89 | password: password + '1', 90 | }) 91 | .end(function (err, res) { 92 | should.not.exist(err); 93 | JSON.parse(res.text).should.containEql({ 94 | status: 'ERROR', 95 | errorMessage: 'The email or password you entered is incorrect', 96 | }); 97 | done(); 98 | }); 99 | }); 100 | it('should sign in successful', function (done) { 101 | request 102 | .post('/auth/login') 103 | .send({ 104 | account: account, 105 | password: password, 106 | }) 107 | .end(function (err, res) { 108 | should.not.exist(err); 109 | JSON.parse(res.text).should.containEql({ status: 'OK' }); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('logout', function (done) { 116 | it('should logout successful', function (done) { 117 | request.post('/auth/logout').end(function (err, res) { 118 | should.not.exist(err); 119 | res.text.should.equal('ok'); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('link', function (done) { 126 | it('should link successful', function (done) { 127 | request.get('/auth/link').end(function (err, res) { 128 | should.not.exist(err); 129 | res.headers.location.should.equal('/auth/login'); 130 | done(); 131 | }); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/api/init/database.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2'); 2 | const should = require('should'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const { config } = require('../../../bin/core/config'); 7 | const { redisClient } = require('../../../bin/core/utils/connections'); 8 | 9 | describe('api/init/database.js', function () { 10 | describe('create database', function (done) { 11 | it('should create database successful', function (done) { 12 | var connection = mysql.createConnection({ 13 | host: config.db.host, 14 | user: config.db.username, 15 | password: config.db.password, 16 | multipleStatements: true, 17 | }); 18 | connection.connect(); 19 | connection.query( 20 | `DROP DATABASE IF EXISTS ${config.db.database};CREATE DATABASE IF NOT EXISTS ${config.db.database}`, 21 | function (err, rows, fields) { 22 | should.not.exist(err); 23 | done(); 24 | }, 25 | ); 26 | connection.end(); 27 | }); 28 | }); 29 | 30 | describe('flushall redis', function (done) { 31 | it('should flushall redis successful', function (done) { 32 | redisClient 33 | .flushAll() 34 | .then(function (reply) { 35 | reply.toLowerCase().should.equal('ok'); 36 | done(); 37 | }) 38 | .catch(function (err) { 39 | should.not.exist(err); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('import data from sql files', function (done) { 45 | var connection; 46 | before(function () { 47 | connection = mysql.createConnection({ 48 | host: config.db.host, 49 | user: config.db.username, 50 | password: config.db.password, 51 | database: config.db.database, 52 | multipleStatements: true, 53 | }); 54 | connection.connect(); 55 | }); 56 | 57 | after(function () { 58 | connection.end(); 59 | }); 60 | 61 | it('should import data codepush-all.sql successful', function (done) { 62 | var sql = fs.readFileSync( 63 | path.resolve(__dirname, '../../../sql/codepush-all.sql'), 64 | 'utf-8', 65 | ); 66 | connection.query(sql, function (err, results) { 67 | should.not.exist(err); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byronigoe/code-push-server/3ff57ef6ee878fb2e97978944d1807b9a2f0abe2/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "target": "es2020", 6 | "outDir": "bin", 7 | "lib": ["es5", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020"], 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "noEmitOnError": true, 11 | "allowSyntheticDefaultImports": true, 12 | "allowJs": true 13 | }, 14 | "include": ["src/**/*.*"] 15 | } 16 | -------------------------------------------------------------------------------- /views/auth/confirm.pug: -------------------------------------------------------------------------------- 1 | extends ../common 2 | 3 | block css 4 | link(rel='stylesheet', href='/stylesheets/signin.css') 5 | 6 | block content 7 | .container 8 | form#form.form-signin(method="post") 9 | h2.form-signin-heading Please confirm your token and create a password 10 | .form-group 11 | label.sr-only(for="inputEmail") Email address 12 | input#inputEmail.form-control(type="text" name="email" placeholder="Email address" value=email required readonly=email ? true : false) 13 | .form-group 14 | label.sr-only(for="inputToken") Token 15 | input#inputToken.form-control(type="text" name="token" placeholder="Token" required autofocus) 16 | .form-group 17 | label.sr-only(for="inputPassword") Create password 18 | input#inputPassword.form-control(type="password" name="password" placeholder="Password" required) 19 | a#submitBtn.btn.btn-lg.btn-primary.btn-block Confirm 20 | 21 | #myModal.modal.fade.bs-example-modal-sm(tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel") 22 | .modal-dialog.modal-md 23 | .modal-content 24 | .modal-header 25 | button.close(data-dismiss="modal" aria-label="Close") 26 | span(aria-hidden="true") × 27 | h4#mySmallModalLabel.modal-title Registration complete 28 | .modal-body Please proceed to login. 29 | .modal-footer 30 | button.btn.btn-default(type="button" data-dismiss="modal") Close 31 | button#okBtn.btn.btn-primary(type="button" data-dismiss="modal") OK 32 | block js 33 | script(). 34 | $('#okBtn').on('click', function () { 35 | location.href = '/auth/login?email=' + $('#inputEmail').val(); 36 | }); 37 | 38 | var submit = false; 39 | $('#submitBtn').on('click', function () { 40 | if (submit) { 41 | return ; 42 | } 43 | console.log($('#form').serializeArray()); 44 | submit = true; 45 | $.ajax({ 46 | type: 'post', 47 | data: $('#form').serializeArray(), 48 | url: "/users/", 49 | dataType: 'json', 50 | success: function (data) { 51 | if (data.status == "OK") { 52 | myModal = $('#myModal'); 53 | myModal.modal('show'); 54 | submit = false; 55 | } else { 56 | alert(data.message); 57 | submit = false; 58 | } 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /views/auth/login.pug: -------------------------------------------------------------------------------- 1 | extends ../common 2 | 3 | block css 4 | link(rel='stylesheet', href='/stylesheets/signin.css') 5 | 6 | block content 7 | .container 8 | form#form.form-signin(method="post") 9 | h2.form-signin-heading #{__('Please sign in')} 10 | label.sr-only(for="inputEmail") #{__('email address')}/#{__('username')} 11 | input#inputEmail.form-control(type="text" name="account" placeholder=`${__('email address')}/${__('username')}` value=email required autofocus) 12 | label.sr-only(for="inputPassword") #{__('password')} 13 | input#inputPassword.form-control(type="password" name="password" placeholder=`${__('password')}` required) 14 | a#submitBtn.btn.btn-lg.btn-primary.btn-block #{__('Log in')} 15 | if showRegister 16 | a#registerBtn.btn.btn-lg.btn-primary.btn-block(href="/auth/register" type="button") #{__('Register')} 17 | 18 | block js 19 | script(). 20 | function onLoggedIn() { 21 | var query = parseQuery() 22 | if (query.hostname) { 23 | // come from code-push-cli login 24 | location.href = '/tokens/' + location.search; 25 | } else { 26 | location.href = '/'; 27 | } 28 | } 29 | 30 | if (getAccessToken()) { 31 | onLoggedIn(); 32 | } 33 | 34 | var submit = false; 35 | $('#submitBtn').on('click', function () { 36 | if (submit) { 37 | return ; 38 | } 39 | submit = true; 40 | $.ajax({ 41 | type: 'post', 42 | data: $('#form').serializeArray(), 43 | url: $('#form').attr('action'), 44 | dataType: 'json', 45 | success: function (data) { 46 | if (data.status == "OK") { 47 | localStorage.setItem('auth', data.results.tokens) 48 | submit = false; 49 | onLoggedIn(); 50 | } else { 51 | alert(data.message); 52 | submit = false; 53 | } 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /views/auth/password.pug: -------------------------------------------------------------------------------- 1 | extends ../common 2 | 3 | block content 4 | .container(style="margin-top:30px;") 5 | form#form.col-md-5.col-md-offset-3(method="post") 6 | .form-group 7 | label.sr-only(for="inputPassword") #{__('old password')} 8 | input#inputPassword.form-control(type="password" name="oldPassword" placeholder=`${__('old password')}` required) 9 | .form-group 10 | label.sr-only(for="inputNewPassword") #{__('new password')} 11 | input#inputNewPassword.form-control(type="password" name="newPassword" placeholder=`${__('new password')}` required) 12 | .form-group 13 | a#submitBtn.btn.btn-lg.btn-primary.btn-block #{__('Change Password')} 14 | 15 | block js 16 | script(). 17 | ensureLogin(); 18 | 19 | var submit = false; 20 | $('#submitBtn').on('click', function () { 21 | if (submit) { 22 | return ; 23 | } 24 | submit = true; 25 | var accessToken = getAccessToken(); 26 | var oldPassword = $('#inputPassword').val(); 27 | var newPassword = $('#inputNewPassword').val(); 28 | $.ajax({ 29 | type: 'patch', 30 | data: JSON.stringify({ oldPassword: oldPassword, newPassword: newPassword }), 31 | contentType: 'application/json;charset=utf-8', 32 | headers: { 33 | Authorization : 'Bearer ' + accessToken, 34 | }, 35 | url: '/users/password', 36 | dataType: 'json', 37 | success: function (data) { 38 | if (data.status == "OK") { 39 | alert("#{__('change success')}"); 40 | logout(); 41 | } else if (data.status == 401) { 42 | alert('token invalid'); 43 | logout(); 44 | } else { 45 | alert(data.message); 46 | } 47 | submit = false; 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /views/auth/register.pug: -------------------------------------------------------------------------------- 1 | extends ../common 2 | 3 | block css 4 | link(rel='stylesheet', href='/stylesheets/signin.css') 5 | 6 | block content 7 | .container 8 | form#form.form-signin(method="post") 9 | h2.form-signin-heading Please register 10 | .form-group 11 | label.sr-only(for="inputEmail") Email address 12 | input#inputEmail.form-control(type="text" name="email" placeholder="Email address" value=email required autofocus) 13 | a#submitBtn.btn.btn-lg.btn-primary.btn-block Register 14 | 15 | block js 16 | script(). 17 | var submit = false; 18 | $('#submitBtn').on('click', function () { 19 | if (submit) { 20 | return ; 21 | } 22 | submit = true; 23 | $.ajax({ 24 | type: 'post', 25 | data: $('#form').serializeArray(), 26 | url: "/users/registerCode", 27 | dataType: 'json', 28 | success: function (data) { 29 | if (data.status == "OK") { 30 | let email = $('#inputEmail').val(); 31 | location.href = '/auth/confirm?email=' + email; 32 | submit = false; 33 | } else { 34 | alert(data.message); 35 | submit = false; 36 | } 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /views/common.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | meta(name="keywords" content="code-push-server,code-push,react-native,cordova") 6 | meta(name="description" content="CodePush service is hotupdate services which adapter react-native-code-push and cordova-plugin-code-push") 7 | link(rel='stylesheet', href='/js/bootstrap-3.3.7/css/bootstrap.min.css') 8 | link(rel='stylesheet', href='/stylesheets/common.css') 9 | block css 10 | body 11 | block content 12 | 13 | script(src='/js/jquery-3.1.1.min.js') 14 | script(src='/js/bootstrap-3.3.7/js/bootstrap.min.js') 15 | script(). 16 | function getAccessToken() { 17 | return localStorage.getItem('auth'); 18 | } 19 | function ensureLogin() { 20 | if (!getAccessToken()) { 21 | window.location.href = '/auth/login'; 22 | } 23 | } 24 | function logout() { 25 | localStorage.removeItem('auth'); 26 | location.href = '/auth/login'; 27 | } 28 | function parseQuery() { 29 | query = location.search.substring(1); 30 | var vars = query.split('&'); 31 | var rs = {}; 32 | for (var i = 0; i < vars.length; i++) { 33 | var pair = vars[i].split('='); 34 | rs[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 35 | } 36 | return rs; 37 | } 38 | block js 39 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends common 2 | 3 | block content 4 | .site-notice react native / cordova #{__('hot update server')} 5 | h1(style="text-align: center;")= title 6 | p(style="text-align: center;") Welcome to #{title} 7 | .site-notice 8 | a.btn.btn-primary(href="/tokens" type="button") #{__('Obtain')} token 9 | a.btn.btn-primary.col-md-offset-1(href="/auth/password" type="button") #{__('Change Password')} 10 | a#logoutBtn.btn.btn-primary.col-md-offset-1(href="#" type="button") #{__('Logout')} 11 | 12 | block js 13 | script(). 14 | ensureLogin(); 15 | 16 | $('#logoutBtn').on('click', logout); 17 | -------------------------------------------------------------------------------- /views/tokens.pug: -------------------------------------------------------------------------------- 1 | extends common 2 | 3 | block content 4 | h1(style="text-align: center;")= title 5 | .site-notice 6 | a#submitBtn.btn.btn-lg.btn-primary #{__('Obtain')} token 7 | .form-group 8 | #tipsSuccess(style="display:none") 9 | h2(style="text-align: center;") Authentication succeeded. 10 | h2(style="text-align: center;") Please copy and paste this access key to the command window: 11 | .form-group 12 | .col-sm-offset-3.col-sm-6 13 | input#key.form-control(style="display:none" readonly) 14 | br 15 | .form-group 16 | #tipsClose(style="display:none") 17 | h2(style="text-align: center;") After doing so, please close this browser. 18 | 19 | block js 20 | script(). 21 | ensureLogin(); 22 | var submit = false; 23 | 24 | $('#submitBtn').on('click', function () { 25 | if (submit) { 26 | return ; 27 | } 28 | submit = true; 29 | var query = parseQuery(); 30 | var createdBy = query.hostname; 31 | var time = (new Date()).getTime(); 32 | if (!createdBy) { 33 | createdBy = 'Login-' + time; 34 | } 35 | 36 | // TODO: make ttl and friendlyNamee configurable 37 | var postParams = { 38 | createdBy: createdBy, 39 | friendlyName: "Login-" + time, 40 | ttl: 60*60*24*30*1000, 41 | description: "Login-" + time, 42 | isSession: true 43 | }; 44 | var accessToken = getAccessToken(); 45 | $.ajax({ 46 | type: 'post', 47 | data: postParams, 48 | headers: { 49 | Authorization : 'Bearer ' + accessToken 50 | }, 51 | url: '/accessKeys', 52 | dataType: 'json', 53 | success: function (data) { 54 | submit = false; 55 | $('#tipsSuccess').show(); 56 | $('#key').val(data.accessKey.name); 57 | $('#key').show(); 58 | $('#tipsClose').show(); 59 | }, 60 | error: function(XMLHttpRequest, textStatus, errorThrown) { 61 | submit = false; 62 | if (errorThrown == 'Unauthorized') { 63 | alert(`#{__('please login again')}!`); 64 | location.href = '/auth/login' 65 | }else { 66 | alert(errorThrown); 67 | } 68 | } 69 | }); 70 | }); 71 | --------------------------------------------------------------------------------