[refactor] Refactored app using koa

This commit is contained in:
Peter Sykora 2018-05-20 16:08:08 +02:00
parent 3a06e5d54f
commit bbec2a1602
71 changed files with 10521 additions and 1973 deletions

View File

@ -1,17 +0,0 @@
{
"extends": ["eslint:recommended", "react-app"],
"parserOptions": {
"ecmaVersion": 8,
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
},
"plugins": ["react"],
"env": {
"es6": true,
"node": true
},
"rules": {
"no-console": ["error", { "allow": ["log", "warn", "error"] }]
}
}

7
.example-env Normal file
View File

@ -0,0 +1,7 @@
NODE_ENV = development
PORT = 3000
SECRET = secret
JWT_SECRET = secret
#DB_CLIENT = sqlite3 | pg
#DB_CONNECTION = postgres://user:password@localhost:5432/db_name

30
.gitignore vendored
View File

@ -1,8 +1,26 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
/bower_components
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# System Files
.DS_Store
Thumbs.db
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# tmp
data
.env
node_modules
.projectile
.vscode
.idea
/.vs/
/bin/

View File

@ -1,4 +1,4 @@
FROM node:9
FROM node:10
# Create app directory
WORKDIR /usr/src/app
@ -14,7 +14,7 @@ RUN npm install --only=production
COPY . .
VOLUME /usr/src/app/data
RUN npm run reset-db
RUN npm run db:schema
EXPOSE 3000
CMD [ "npm", "start" ]

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: yarn start

209
README.md
View File

@ -1,182 +1,65 @@
# License server
# ![RealWorld Example App](logo.png)
This is reference implementation of license server that allows per-machine software licensing while it also manages software and data updates for the
client machines. The protocol is described below.
> ### Example Node.Js (Koa.js + Knex) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API.
## Preactivation request
This repo is functionality complete — PRs and issues welcome!
When application is run for first time it is not activated. First it sends its system params (Fletcher64 hashes) to the server to check
if the computer has not been preactivated
This codebase was created to demonstrate a fully fledged fullstack application built with **Koa.js + Knex** including CRUD operations, authentication, routing, pagination, and more.
```http
POST /activate0 HTTP/1.1
We've gone to great lengths to adhere to the **Koa.js + Knex** community styleguides & best practices.
For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
# Getting started
## Installation
1. Instal [Node.JS](https://nodejs.org/en/download/package-manager/) latest version
2. Clone this repo
3. Install dependencies, just run in project folder: `npm i` or `yarn`
## Usage
1. run `npm start` to start server
## Testing
1. run `npm test` for tests
## Server Configuration (optional)
You can use `.env` file, to configure project like this:
```
NODE_ENV = development
PORT = 3000
SECRET = secret
JWT_SECRET = secret
DB_CLIENT = sqlite3
#DB_CONNECTION = postgres://user:password@localhost:5432/db_name
```
Example request body:
```json
{
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
}
}
```
you can just copy `.example-env`
The server checks if the system is preactivated by checking if any of the system parameters exists in `PreactivationParams` table.
For example for license `XXXX-YYYY` there can be single entry for `biosSerialNum` parameters which matches the system of the requestor.
Therefore the activation will happen using the license number `XXXX-YYYY` and the system will response with license file that the client
system should store locally. We will describe the structure later.
## Variables description
Example Response body:
```json
{
"success": true,
"licenseFile": "fG2HUWhE5kT10Ono3qgO/pzHNU1KAnUlEszz3B1pP5FdSQlIukg3P3xUgfHDQX1OBuAFH68WeXe2T0YP1dbjSsAyDJfpUltJnncwMMLOkfR3YbvyAVNmScgASLWwyQxQAVID6GOQ2weVNo3tAcbj2Ted7rx0HL36seSzyY5xuV/SnfJN6q5acqyJmibQcsrPQLZBvIb00Cy9KENnkVFVH70kC406pKxVZ3Ghqg8vTxgBK6sdbwqd7XrpupcQ4frwwm/NPesCIBYRG+6C/9oMroQoPZ+NEzOYQq2PADOZgOSvaMp4FUNe30IqNckobQO7N2TmW4BJDKVzhG5OYQRfs+xvzG0lF5gOBtmBhDf3yStgJj++jc2pNwe0rDSP2J+Yjs+V8wepyuCJ08cfZOh1feqnzmAarzAD80W8wRFd9U2lHDsyFt7Ke/3aPX454jxqITn/Wu6MK0paX8YAATWWAONyUsWTdA3UkJm45gNHJibcQcZLwKErZEPr2XBNy8nrImHRGFFtr3KC"
}
```
`NODE_ENV` - specify env: development/production/test. `development` by default
If the Preactivation is not available for given client, the system will response with `{ success: false }`
`NODE_PORT` - specify port: `3000` by default
## Activation request
`NODE_SECRET` - custom secret for generating passwords. `secret` by default
If the system is not preactivated the client application should ask use to enter the license key (Or the license key should be somehow
received by the client system. The license key is 24 characters in base32 (RFC 4648) format. For simplicity it can be sent to user as
e.g. `T3HZ-IFAT-HLN5-2I57-HAGL-V24R` which is easy to read and enter manually. Dashes in between the characters are ignored.
`JWT_SECRET` - custom secret for generating jwt tokens. `secret` secret by default
The client is supposed to send JSON request similar as in preactivation with just one parameter added `licenseNumber` which should be
uppercase without dashes so the server can directly query the database.
`DB_CLIENT` - database to use. `pg` - postgress or `sqlite3`. `sqlite3` by default
### Example
```http
POST /activate HTTP/1.1
Host: localhost:3000
Accept: application/json
Content-Type: application/json
Content-Length: 219
Charsets: utf-8
```
Request data:
```json
{
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"licenseNumber": "JK33BTBSBKSKV63YEVLMQMBZ"
}
```
Response:
```
{
"success": true,
"licenseFile": "CmiN19MDaeCtA4L+cqhrnL71GWjBqTA6cqP8pUyfwRY+r/s/CyODkgHnO9eg3yaLLNxnNFMOnZOtxgtz8hNMNUIsTAKzus068sz9dJhV2yLrmkvhi1KjEJdOua4ZXuKSzjGKxM+VFXokFfFTqxvVpPt5sMkwq9kG/cZSwpBw7POhR+ncHeF11jjkbKVUnVgjGq8EDHDQFANYAVB3qbo7PY9CG3Gm25nORMUMpqwKieadVmklBZYs09EUqqwxAxxpD44Hw2DaRwoaVMuKTC//wH+3oS3zoL2mx+panJ/HPCN7ZtdBje+v6HSlfUoHgCHpFrr0+9/YqvxAQBWz0Q8dSyzyHkuzEkb4Ob7uWSeVhmKJ1TfzX8CAchth9f4CLCuLdzmCpRVvIAM6ZS4o0t3n/3AAJAmBuKFr9OrHTnN1EgXUYB1TVjiHqvBXR1jEyvYyvkAC4CTel7/Y1LRK2y6mq5is/uLcuKyKpWItMr5p9/3qJAove7tZpT1KrNLYaIPGudIwcnYin8EBAK09uUrqRg=="
}
```
## Check for updates
Another functionality that server supports is check for updates.
Client should send versions of its application modules (e.g. application, data) together with its activation ID, system parameters.
Server checks whether the system with given activation ID and system parameters is still valid and if yes it looks for updates
of the licensed modules. The typical request should look like this:
```json
{
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"activationId": "f0d68f64-4bc5-33b0-6ab3-e9b446baea08",
"moduleVersions": {
"coc-testdata": 2
}
}
```
The response contains `success: true` if the system is still activated with supplied activation number and system parameters.
If the number of licensed modules has changed since the activation, the server can reactivate the installation and return back also new `licenseFile`.
If there are newer versions available there should be list of updates attached. Here is the explanation of update parameters that are not obvious:
* flag - bit flag (bit 1: signals whether it is incremental update, bit 2: signals whether restart is required after update)
* checksum - SHA256 checksum of the zip file
```json
{
"success": true,
"moduleUpdates": [
{
"moduleId": "coc-testdata",
"version": 3,
"flag": 0,
"checksum": "6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731",
"updateUri": "http://localhost:3000/static/testsite-v3.zip",
"instPath": "data"
},
{
"moduleId": "coc-testdata",
"version": 4,
"flag": 1,
"checksum": "a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040",
"updateUri": "http://localhost:3000/static/testsite-v4-incremental.zip",
"instPath": "data"
}
]
}
```
`DB_CONNECTION` - db connection string for `postgress` database.
## Fixtures (optional)
## License file
1. load fixtures: `npm run db:load` (it uses settings from `.env`). Don't forget to set `NODE_ENV`.
License file is zlib deflated and AES256-GCM encrypted of following structure
## Styleguide
[![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)
```json
{
"data": "{\"activationId\":\"e8cb99bc-9827-5c3a-b944-94ac97c81366\",\"appId\":\"coc\",\"systemParams\":{\"biosSerialNum\":\"8690a8fb436070a9\",\"computerUUID\":\"13cfc3b6f8f7fdd2\",\"diskSerialNum\":\"63a58b9728485155\",\"nicMac\":\"4b2856a1e9e8f43e\",\"osId\":\"ec4fe2f3023d1f21\"},\"licensedModules\":[\"coc-engine\",\"coc-testdata\"],\"nonce\":\"pSvFZJ8q3tqBzp0pLEmCSg==\"}",
"signature": "304c02240163a4b1a6e9a366672df1a17418ab44fb3471fefc4234b4220de8079d0a59ee3c05ce3502240346e0d66d388e3f6cef1cffe14c6be6930a858c72190359fa57aa755c5767b3688d9d88"
}
```
The signature is ECDSA+SHA256 signature of the serialized data node. The data node extracts further below:
```json
{
"activationId": "e8cb99bc-9827-5c3a-b944-94ac97c81366",
"appId": "coc",
"systemParams": {
"biosSerialNum": "8690a8fb436070a9",
"computerUUID": "13cfc3b6f8f7fdd2",
"diskSerialNum": "63a58b9728485155",
"nicMac": "4b2856a1e9e8f43e",
"osId": "ec4fe2f3023d1f21"
},
"licensedModules": [
"coc-engine",
"coc-testdata"
]
}
```
As you can see the license file specifies activation number with all system parameters the license is activated to.
It also lists modules that are licensed for the given system.
Client system should save the license file received from the server localy and should upon every start check whether
the system parameters are the same as specified in the license file. The license file is protected from tempering by
ECDSA (public/private key cryptography) signature while the client only knows the public key of the server.
## Data model
# How it works
> Describe the general architecture of your app here

View File

@ -1,13 +0,0 @@
// Ensure require('dotenv').config() is run before this module is required
exports.NODE_ENV = process.env.NODE_ENV || 'development'
exports.PORT = Number.parseInt(process.env.PORT, 10) || 3000
exports.DATABASE_FILE =
process.env.DATABASE_URL || './data/license-server.db'
// //////////////////////////////////////////////////////////
// Output config object in development to help with sanity-checking
if (exports.NODE_ENV === 'development' || exports.NODE_ENV === 'test') {
console.log(exports)
}

View File

@ -1 +0,0 @@
require('./src').start()

53
knexfile.js Normal file
View File

@ -0,0 +1,53 @@
// Update with your config settings.
module.exports = {
development: {
client: 'mysql',
connection: {
database: 'licserver',
user: 'root',
password: ''
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
},
staging: {
client: 'postgresql',
connection: {
database: 'my_db',
user: 'username',
password: 'password'
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
},
production: {
client: 'postgresql',
connection: {
database: 'my_db',
user: 'username',
password: 'password'
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
}
};

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{8dfa872b-4368-4176-811a-6d3d9665e54b}</ProjectGuid>
<ProjectGuid>{cdb93689-38b1-497c-a17e-3fea4607a7e8}</ProjectGuid>
<ProjectHome />
<ProjectView>ShowAllFiles</ProjectView>
<StartupFile />
@ -12,40 +12,57 @@
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
<ItemGroup>
<Content Include="package-lock.json" />
<Compile Include="src\controllers\apiV1-controller.js" />
<Compile Include="src\controllers\modules-controller.js" />
<Compile Include="src\controllers\products-controller.js" />
<Compile Include="src\lib\generateLicenseFile.js" />
<Compile Include="src\lib\licenseUtil.js" />
<Compile Include="src\migrations\20180428115300_init.js" />
<Compile Include="src\routes\apiV1-router.js" />
<Compile Include="src\routes\products-router.js" />
<Compile Include="src\seeds\01-products.js" />
<Compile Include="src\seeds\02-modules.js" />
<Compile Include="src\seeds\03-licenses.js" />
<Compile Include="src\seeds\04-licensedModules.js" />
<Compile Include="src\seeds\05-preactivationParams.js" />
<Content Include="logo.png" />
<Content Include="package.json" />
<Content Include="readme.md" />
<Compile Include="src\app.js" />
<Compile Include="src\bin\server.js" />
<Compile Include="src\config\index.js" />
<Compile Include="src\config\knexfile.js" />
<Compile Include="src\controllers\index.js" />
<Compile Include="src\lib\constants.js" />
<Compile Include="src\lib\errors.js" />
<Compile Include="src\lib\relations-map.js" />
<Compile Include="src\middleware\auth-required-middleware.js" />
<Compile Include="src\middleware\camelize-middleware.js" />
<Compile Include="src\middleware\db-middleware.js" />
<Compile Include="src\middleware\error-middleware.js" />
<Compile Include="src\middleware\jwt-middleware.js" />
<Compile Include="src\middleware\pager-middleware.js" />
<Compile Include="src\middleware\user-middleware.js" />
<Compile Include="src\routes\index.js" />
<Compile Include="src\schemas\index.js" />
<Compile Include="src\schemas\time-stamp-schema.js" />
</ItemGroup>
<ItemGroup>
<Folder Include="backend" />
<Folder Include="license-server" />
<Folder Include="sql\" />
<Folder Include="src\" />
<Folder Include="src\db\" />
<Folder Include="src\routes\" />
<Folder Include="src\util\" />
</ItemGroup>
<ItemGroup>
<Compile Include="config.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="index.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="sql\reset-db.js" />
<Compile Include="src\util\licenseUtil.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="src\db\pool.js" />
<Compile Include="src\index.js">
<SubType>Code</SubType>
</Compile>
<Compile Include="src\routes\index.js">
<SubType>Code</SubType>
</Compile>
<Folder Include="src" />
<Folder Include="src\bin" />
<Folder Include="src\config" />
<Folder Include="src\controllers" />
<Folder Include="src\lib" />
<Folder Include="src\middleware" />
<Folder Include="src\migrations" />
<Folder Include="src\routes" />
<Folder Include="src\schemas" />
<Folder Include="src\seeds" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<!--Do not delete the following Import Project. While this appears to do nothing it is a marker for setting TypeScript properties before our import that depends on them.-->

View File

@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
VisualStudioVersion = 15.0.27428.2011
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "license-server", "license-server.njsproj", "{8DFA872B-4368-4176-811A-6D3D9665E54B}"
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "license-server", "license-server.njsproj", "{CDB93689-38B1-497C-A17E-3FEA4607A7E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -11,15 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.Build.0 = Release|Any CPU
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C030B8B7-C3CC-4C52-97CA-1C3852CB83D9}
SolutionGuid = {2D183C48-BF8D-4B4D-920B-C5C498E25DC1}
EndGlobalSection
EndGlobal

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

8613
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,72 @@
{
"name": "license-server",
"name": "node-koa-realworld-starter-kit",
"version": "1.0.0",
"description": "License Server",
"description": "conduit on koa.js",
"main": "index.js",
"repository": "git@github.com:dimonnwc3/node-koa-realworld-starter-kit.git",
"author": "Dmitrii Solovev <dimonnwc3@gmail.com>",
"license": "ISC",
"scripts": {
"start": "cross-env NODE_PATH=src node src/bin/server.js",
"db:schema": "knex migrate:latest --knexfile src/config/knexfile.js",
"db:load": "knex seed:run --knexfile src/config/knexfile.js",
"lint": "standard | snazzy",
"lint:fix": "standard --fix | snazzy",
"test": "jest",
"test:watch": "jest --watch"
},
"pre-commit": [
"lint"
],
"dependencies": {
"content-type": "^1.0.4",
"bcrypt": "^2.0.1",
"cross-env": "^5.1.4",
"date-fns": "^1.29.0",
"dotenv": "^5.0.1",
"es6-denodeify": "^0.1.5",
"guid": "0.0.12",
"koa": "^2.5.0",
"koa-better-static2": "^1.0.2",
"http-shutdown": "^1.2.0",
"humps": "^2.0.1",
"join-js": "^1.0.0",
"jsonwebtoken": "^8.2.1",
"kcors": "^2.2.1",
"knex": "^0.14.6",
"koa": "^2.5.1",
"koa-bodyparser": "^4.2.0",
"koa-bouncer": "^6.0.4",
"koa-compress": "^2.0.0",
"koa-helmet": "^3.3.0",
"koa-logger": "^3.1.0",
"koa-mount": "^3.0.0",
"koa-route": "^3.2.0",
"koa-helmet": "^4.0.0",
"koa-jwt": "^3.3.1",
"koa-logger": "^3.2.0",
"koa-response-time": "^2.0.0",
"koa-router": "^7.4.0",
"sqlite": "^2.9.1"
"lodash": "^4.17.10",
"mysql": "^2.15.0",
"pg": "^7.4.1",
"qs": "^6.5.1",
"request": "^2.85.0",
"request-promise": "^4.2.2",
"slug": "^0.9.1",
"uuid": "^3.2.1",
"validator": "^9.4.1",
"winston": "^2.4.2",
"yup": "^0.24.1"
},
"devDependencies": {},
"scripts": {
"start": "node index.js",
"reset-db": "node ./sql/reset-db",
"test": "echo \"Error: no test specified\" && exit 1"
"devDependencies": {
"faker": "^4.1.0",
"jest": "^22.4.3",
"pre-commit": "^1.2.2",
"snazzy": "^7.1.1",
"standard": "^11.0.1"
},
"engines": {
"node": ">=9.x"
},
"author": "Peter Sykora",
"license": "ISC"
"jest": {
"testPathIgnorePatterns": [
"<rootDir>[/\\\\](docs|node_modules)[/\\\\]"
],
"bail": true,
"testMatch": [
"**/__tests__/**/*index.js?(x)",
"**/?(*.)(spec|test)index.js?(x)"
],
"modulePaths": [
"src"
]
}
}

View File

@ -1,55 +0,0 @@
require('dotenv').config()
// Node
const path = require('path')
const { promisify } = require('util')
const readFile = promisify(require('fs').readFile)
// 1st
const config = require('../config')
const { pool } = require('../src/db/pool')
// //////////////////////////////////////////////////////////
// Sanity check: Ensure this isn't being run in production
if (config.NODE_ENV !== 'development') {
throw new Error('[reset_db] May only reset database in development mode')
}
// //////////////////////////////////////////////////////////
function slurpSql(filePath) {
const relativePath = '../sql/' + filePath
const fullPath = path.join(__dirname, relativePath)
return readFile(fullPath, 'utf8')
}
async function seed() {
console.log('Resetting the database...')
await (async () => {
const sql = await slurpSql('schema.sql')
console.log('-- Executing schema.sql...')
const conn = await pool;
console.log(conn)
console.log(await conn.get('SELECT 1'))
console.log(await conn.exec(sql));
console.log(await conn.get('SELECT * FROM Activation'));
})()
// await (async () => {
// const sql = await slurpSql('seeds.sql')
// console.log('-- Executing seeds.sql...')
// await pool._query(sql)
// })()
}
seed().then(
() => {
console.log('Finished resetting db')
process.exit(0)
},
err => {
console.error('Error:', err, err.stack)
process.exit(1)
}
)

View File

@ -1,301 +0,0 @@
--
-- File generated with SQLiteStudio v3.1.1 on Tue Mar 13 19:16:57 2018
--
-- Text encoding used: UTF-8
--
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
-- Table: Activation
DROP TABLE IF EXISTS Activation;
CREATE TABLE Activation (
activationId VARCHAR (40) PRIMARY KEY,
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
activatedOn DATETIME NOT NULL,
deactivatedOn DATETIME,
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
-- Table: ActivationParams
DROP TABLE IF EXISTS ActivationParams;
CREATE TABLE ActivationParams (
activationId VARCHAR (40) NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR NOT NULL,
flag INTEGER NOT NULL
DEFAULT (0),
PRIMARY KEY (
activationId,
paramId
),
FOREIGN KEY (
activationId
)
REFERENCES Activation (activationId)
);
-- Table: CheckLog
DROP TABLE IF EXISTS CheckLog;
CREATE TABLE CheckLog (
checkLogId VARCHAR (40) PRIMARY KEY
NOT NULL,
activationId VARCHAR (40) NOT NULL,
checkedOn DATETIME NOT NULL,
FOREIGN KEY (
activationId
)
REFERENCES Activation (activationId)
);
-- Table: CheckLogParams
DROP TABLE IF EXISTS CheckLogParams;
CREATE TABLE CheckLogParams (
checkLogId VARCHAR (40) REFERENCES CheckLog (checkLogId)
NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR NOT NULL,
PRIMARY KEY (
checkLogId,
paramId
)
);
-- Table: CheckLogVersion
DROP TABLE IF EXISTS CheckLogVersion;
CREATE TABLE CheckLogVersion (
checkLogId VARCHAR (40) NOT NULL
REFERENCES CheckLog (checkLogId),
moduleId VARCHAR (40) NOT NULL,
version INTEGER NOT NULL,
PRIMARY KEY (
checkLogId,
moduleId
)
);
-- Table: License
DROP TABLE IF EXISTS License;
CREATE TABLE License (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
customerId VARCHAR (20) NOT NULL,
PRIMARY KEY (
appId,
licenseNum
)
);
INSERT INTO License (
appId,
licenseNum,
customerId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'111'
);
INSERT INTO License (
appId,
licenseNum,
customerId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'111'
);
-- Table: LicensedModule
DROP TABLE IF EXISTS LicensedModule;
CREATE TABLE LicensedModule (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
moduleId VARCHAR (40) NOT NULL,
PRIMARY KEY (
appId,
licenseNum,
moduleId
),
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'coc-engine'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'JK33BTBSBKSKV63YEVLMQMBZ',
'coc-testdata'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'coc-testdata'
);
INSERT INTO LicensedModule (
appId,
licenseNum,
moduleId
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'coc-engine'
);
-- Table: ModuleUpdate
DROP TABLE IF EXISTS ModuleUpdate;
CREATE TABLE ModuleUpdate (
moduleId VARCHAR (40) NOT NULL,
version INTEGER NOT NULL,
checksum VARCHAR (32) NOT NULL,
updateUri VARCHAR NOT NULL,
instPath VARCHAR NOT NULL,
flag INTEGER NOT NULL
DEFAULT (0),
PRIMARY KEY (
moduleId,
version
)
);
INSERT INTO ModuleUpdate (
moduleId,
version,
checksum,
updateUri,
instPath,
flag
)
VALUES (
'coc-testdata',
2,
'a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040',
'http://localhost:3000/static/testsite-v2-incremental.zip',
'data',
1
);
INSERT INTO ModuleUpdate (
moduleId,
version,
checksum,
updateUri,
instPath,
flag
)
VALUES (
'coc-testdata',
1,
'6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731',
'http://localhost:3000/static/testsite-v1.zip',
'data',
0
);
-- Table: PreactivationParams
DROP TABLE IF EXISTS PreactivationParams;
CREATE TABLE PreactivationParams (
appId VARCHAR (20) NOT NULL,
licenseNum VARCHAR (24) NOT NULL,
paramId VARCHAR (20) NOT NULL,
paramValue VARCHAR (20) NOT NULL,
paramOrig VARCHAR (255),
PRIMARY KEY (
appId,
licenseNum,
paramId
),
FOREIGN KEY (
appId,
licenseNum
)
REFERENCES License (appId,
licenseNum)
);
INSERT INTO PreactivationParams (
appId,
licenseNum,
paramId,
paramValue,
paramOrig
)
VALUES (
'coc',
'T3HZIFATHLN52I57HAGLV24R',
'biosSerialNum',
'8690a8fb436070a9',
'R80CW80'
);
-- View: ActiveActivationView
DROP VIEW IF EXISTS ActiveActivationView;
CREATE VIEW ActiveActivationView AS
SELECT *
FROM Activation
WHERE deactivatedOn IS NULL;
-- View: LastFullVersionView
DROP VIEW IF EXISTS LastFullVersionView;
CREATE VIEW LastFullVersionView AS
SELECT moduleId,
MAX(version) AS lastFullVersion
FROM ModuleUpdate
WHERE flag <> 1
GROUP BY moduleId;
COMMIT TRANSACTION;
PRAGMA foreign_keys = on;

69
src/app.js Normal file
View File

@ -0,0 +1,69 @@
const config = require('config')
const http = require('http')
const Koa = require('koa')
const app = new Koa()
app.keys = [config.secret]
require('schemas')(app)
const responseTime = require('koa-response-time')
const helmet = require('koa-helmet')
const logger = require('koa-logger')
const camelizeMiddleware = require('middleware/camelize-middleware')
const error = require('middleware/error-middleware')
const db = require('middleware/db-middleware')
const cors = require('kcors')
const jwt = require('middleware/jwt-middleware')
const bodyParser = require('koa-bodyparser')
const pagerMiddleware = require('middleware/pager-middleware')
const userMiddleware = require('middleware/user-middleware')
const routes = require('routes')
if (!config.env.isTest) {
app.use(responseTime())
app.use(helmet())
}
app.use(logger())
app.use(camelizeMiddleware)
app.use(error)
app.use(db(app))
app.use(cors(config.cors))
app.use(jwt)
app.use(bodyParser(config.bodyParser))
app.use(userMiddleware)
app.use(pagerMiddleware)
app.use(routes.routes())
app.use(routes.allowedMethods())
app.server = require('http-shutdown')(http.createServer(app.callback()))
app.shutDown = function shutDown () {
let err
console.log('Shutdown')
if (this.server.listening) {
this.server.shutdown(error => {
if (error) {
console.error(error)
err = error
}
this.db.destroy()
.catch(error => {
console.error(error)
err = error
})
.then(() => process.exit(err ? 1 : 0))
})
}
}
module.exports = app

41
src/bin/server.js Normal file
View File

@ -0,0 +1,41 @@
require('dotenv').config()
const {server: {port, host}} = require('../config')
const app = require('../app')
process.once('SIGINT', () => app.shutDown())
process.once('SIGTERM', () => app.shutDown())
app.server.listen(port, host)
app.server.on('error', onError)
app.server.on('listening', onListening)
function onError (error) {
if (error.syscall !== 'listen') {
throw error
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
default:
throw error
}
}
function onListening () {
var addr = app.server.address()
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
console.log('Listening on ' + bind)
}

62
src/config/index.js Normal file
View File

@ -0,0 +1,62 @@
const path = require('path')
const _ = require('lodash')
const knexfile = require('./knexfile')
const ROOT = path.resolve(__dirname, '../')
const NODE_ENV = _.defaultTo(process.env.NODE_ENV, 'development')
const isProd = NODE_ENV === 'production'
const isTest = NODE_ENV === 'test'
const isDev = NODE_ENV === 'development'
module.exports = {
server: {
port: normalizePort(_.defaultTo(process.env.PORT, 3000)),
host: _.defaultTo(process.env.HOST, 'localhost'),
root: ROOT,
data: path.join(ROOT, '../', '/data')
},
env: {
isDev,
isProd,
isTest
},
cors: {
origin: '*',
exposeHeaders: ['Authorization'],
credentials: true,
allowMethods: ['GET', 'PUT', 'POST', 'DELETE'],
allowHeaders: ['Authorization', 'Content-Type'],
keepHeadersOnError: true
},
bodyParser: {
enableTypes: ['json']
},
db: knexfile[NODE_ENV],
secret: _.defaultTo(process.env.SECRET, 'secret'),
jwtSecret: _.defaultTo(process.env.JWT_SECRET, 'secret'),
jwtOptions: {
expiresIn: '7d'
}
}
function normalizePort (val) {
var port = parseInt(val, 10)
if (isNaN(port)) {
return val
}
if (port >= 0) {
return port
}
return false
}

40
src/config/knexfile.js Normal file
View File

@ -0,0 +1,40 @@
const path = require('path')
const ROOT = path.resolve(__dirname, '../../')
require('dotenv').config({ path: path.join(ROOT, '.env') })
const { DB_CLIENT, DB_CONNECTION } = process.env
const options = {
client: DB_CLIENT || 'sqlite3',
connection: DB_CONNECTION || { filename: path.join(ROOT, 'data/dev.sqlite3') },
migrations: {
directory: path.join(ROOT, 'src/migrations'),
tableName: 'migrations'
},
debug: false,
seeds: {
directory: path.join(ROOT, 'src/seeds')
},
useNullAsDefault: !DB_CLIENT || DB_CLIENT === 'sqlite3'
}
if (DB_CLIENT && DB_CLIENT !== 'sqlite3') {
options.pool = {
min: 2,
max: 10
}
}
module.exports = {
development: Object.assign({}, options, { debug: true }),
test: Object.assign({}, options, {
connection: DB_CONNECTION || { filename: path.join(ROOT, 'data/test.sqlite3') }
}),
production: Object.assign({}, options, {
connection: DB_CONNECTION || { filename: path.join(ROOT, 'data/prod.sqlite3') }
})
}

View File

@ -0,0 +1,102 @@
const licenseUtil = require('../lib/licenseUtil')
function checkSystemParams(systemParams) {
if (systemParams !== null && typeof systemParams === 'object') {
if (Object.keys(systemParams).length > 1) {
let valid = true
Object.entries(systemParams).forEach(([key, value]) => {
if (typeof key !== 'string' || typeof value !== 'string') {
valid = false
}
})
if (valid) {
return systemParams
}
}
}
throw new Error('Invalid parameters provided')
}
function checkLicenseNumber(licenseNumber) {
if (licenseNumber !== null && typeof licenseNumber === 'string') {
if (licenseNumber.length === 24) {
return licenseNumber
}
}
throw new Error('Invalid license number')
}
function checkActivationId(activationId) {
if (activationId !== null && typeof activationId === 'string') {
if (/^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/.test(activationId)) {
return activationId
}
}
throw new Error('Invalid activation id')
}
function checkProductId(productId) {
if (productId !== null && typeof productId === 'string') {
if (productId.length > 0) {
return (productId)
}
}
throw new Error('Invalid application id')
}
function isInt(value) {
return !isNaN(value) &&
parseInt(Number(value)) == value &&
!isNaN(parseInt(value, 10))
}
function checkModuleVersions(moduleVersions) {
if (moduleVersions !== null && typeof moduleVersions === 'object') {
let valid = true;
Object.entries(moduleVersions).forEach(([key, value]) => {
if (typeof key !== 'string' || !isInt(value)) {
valid = false;
}
})
if (valid) {
return moduleVersions;
}
}
throw new Error('Invalid parameters provided')
}
module.exports = {
async preactivate(ctx) {
const { body } = ctx.request
ctx.body = await licenseUtil.preactivate(
ctx.app.db,
checkProductId(body.productId),
checkSystemParams(body.systemParams)
)
},
async activate(ctx) {
const { body } = ctx.request
ctx.body = await licenseUtil.activate(
ctx.app.db,
checkProductId(body.productId),
checkLicenseNumber(body.licenseNumber),
checkSystemParams(body.systemParams)
)
},
async check(ctx) {
const { body } = ctx.request
ctx.body = await licenseUtil.check(
ctx.app.db,
checkProductId(body.productId),
checkSystemParams(body.systemParams),
checkActivationId(body.activationId),
checkModuleVersions(body.moduleVersions)
)
}
}

View File

@ -0,0 +1,465 @@
const slug = require('slug')
const uuid = require('uuid')
const humps = require('humps')
const _ = require('lodash')
const comments = require('./comments-controller')
const {ValidationError} = require('lib/errors')
const {getSelect} = require('lib/utils')
const {articleFields, userFields, relationsMaps} = require('lib/relations-map')
const joinJs = require('join-js').default
module.exports = {
async bySlug (slug, ctx, next) {
if (!slug) {
ctx.throw(404)
}
const article = await ctx.app.db('articles')
.first()
.where({slug})
if (!article) {
ctx.throw(404)
}
const tagsRelations = await ctx.app.db('articles_tags')
.select()
.where({article: article.id})
let tagList = []
if (tagsRelations && tagsRelations.length > 0) {
tagList = await ctx.app.db('tags')
.select()
.whereIn('id', tagsRelations.map(r => r.tag))
tagList = tagList.map(t => t.name)
}
article.tagList = tagList
article.favorited = false
const author = await ctx.app.db('users')
.first('username', 'bio', 'image', 'id')
.where({id: article.author})
article.author = author
article.author.following = false
const {user} = ctx.state
if (user && user.username !== article.author.username) {
const res = await ctx.app.db('followers')
.where({user: article.author.id, follower: user.id})
.select()
if (res.length > 0) {
article.author.following = true
}
}
let favorites = []
if (user) {
favorites = await ctx.app.db('favorites')
.where({user: user.id, article: article.id})
.select()
if (favorites.length > 0) {
article.favorited = true
}
}
ctx.params.article = article
ctx.params.favorites = favorites
ctx.params.author = author
ctx.params.tagList = tagList
ctx.params.tagsRelations = tagsRelations
await next()
delete ctx.params.author.id
},
async get (ctx) {
const {user} = ctx.state
const {offset, limit, tag, author, favorited} = ctx.query
let articlesQuery = ctx.app.db('articles')
.select(
...getSelect('articles', 'article', articleFields),
...getSelect('users', 'author', userFields),
...getSelect('articles_tags', 'tag', ['id']),
...getSelect('tags', 'tag', ['id', 'name']),
'favorites.id as article_favorited',
'followers.id as author_following'
)
.limit(limit)
.offset(offset)
.orderBy('articles.created_at', 'desc')
let countQuery = ctx.app.db('articles').count()
if (author && author.length > 0) {
const subQuery = ctx.app.db('users')
.select('id')
.whereIn('username', author)
articlesQuery = articlesQuery.andWhere('articles.author', 'in', subQuery)
countQuery = countQuery.andWhere('articles.author', 'in', subQuery)
}
if (favorited && favorited.length > 0) {
const subQuery = ctx.app.db('favorites')
.select('article')
.whereIn(
'user',
ctx.app.db('users').select('id').whereIn('username', favorited)
)
articlesQuery = articlesQuery.andWhere('articles.id', 'in', subQuery)
countQuery = countQuery.andWhere('articles.id', 'in', subQuery)
}
if (tag && tag.length > 0) {
const subQuery = ctx.app.db('articles_tags')
.select('article')
.whereIn(
'tag',
ctx.app.db('tags').select('id').whereIn('name', tag)
)
articlesQuery = articlesQuery.andWhere('articles.id', 'in', subQuery)
countQuery = countQuery.andWhere('articles.id', 'in', subQuery)
}
articlesQuery = articlesQuery
.leftJoin('users', 'articles.author', 'users.id')
.leftJoin('articles_tags', 'articles.id', 'articles_tags.article')
.leftJoin('tags', 'articles_tags.tag', 'tags.id')
.leftJoin('favorites', function () {
this.on('articles.id', '=', 'favorites.article')
.onIn('favorites.user', [user && user.id])
})
.leftJoin('followers', function () {
this.on('articles.author', '=', 'followers.user')
.onIn('followers.follower', [user && user.id])
})
let [articles, [countRes]] = await Promise.all([articlesQuery, countQuery])
articles = joinJs
.map(articles, relationsMaps, 'articleMap', 'article_')
.map(a => {
a.favorited = Boolean(a.favorited)
a.tagList = a.tagList.map(t => t.name)
a.author.following = Boolean(a.author.following)
delete a.author.id
return a
})
let articlesCount = countRes.count || countRes['count(*)']
articlesCount = Number(articlesCount)
ctx.body = {articles, articlesCount}
},
async getOne (ctx) {
ctx.body = {article: ctx.params.article}
},
async post (ctx) {
const {body} = ctx.request
let {article} = body
let tags
const opts = {abortEarly: false}
article.id = uuid()
article.author = ctx.state.user.id
article = await ctx.app.schemas.article.validate(article, opts)
article.slug = slug(_.get(article, 'title', ''), {lower: true})
if (article.tagList && article.tagList.length > 0) {
tags = await Promise.all(
article.tagList
.map(t => ({id: uuid(), name: t}))
.map(t => ctx.app.schemas.tag.validate(t, opts))
)
}
try {
await ctx.app.db('articles')
.insert(humps.decamelizeKeys(_.omit(article, ['tagList'])))
} catch (err) {
if (Number(err.errno) === 19 || Number(err.code) === 23505) {
article.slug = article.slug + '-' + uuid().substr(-6)
await ctx.app.db('articles')
.insert(humps.decamelizeKeys(_.omit(article, ['tagList'])))
} else {
throw err
}
}
if (tags && tags.length) {
for (var i = 0; i < tags.length; i++) {
try {
await ctx.app.db('tags').insert(humps.decamelizeKeys(tags[i]))
} catch (err) {
if (Number(err.errno) !== 19 && Number(err.code) !== 23505) {
throw err
}
}
}
tags = await ctx.app.db('tags')
.select()
.whereIn('name', tags.map(t => t.name))
const relations = tags.map(t => ({
id: uuid(),
tag: t.id,
article: article.id
}))
await ctx.app.db('articles_tags').insert(relations)
}
article.favorited = false
article.author = _.pick(ctx.state.user, ['username', 'bio', 'image'])
article.author.following = false
ctx.body = {article}
},
async put (ctx) {
const {article} = ctx.params
if (article.author.id !== ctx.state.user.id) {
ctx.throw(403, new ValidationError(['not owned by user'], '', 'article'))
}
const {body} = ctx.request
let {article: fields = {}} = body
const opts = {abortEarly: false}
let newArticle = Object.assign({}, article, fields)
newArticle.author = newArticle.author.id
newArticle = await ctx.app.schemas.article.validate(
humps.camelizeKeys(newArticle),
opts
)
if (fields.title) {
newArticle.slug = slug(_.get(newArticle, 'title', ''), {lower: true})
}
newArticle.updatedAt = new Date().toISOString()
try {
await ctx.app.db('articles')
.update(humps.decamelizeKeys(
_.pick(
newArticle,
['title', 'slug', 'body', 'description', 'updatedAt']
)
))
.where({id: article.id})
} catch (err) {
if (Number(err.errno) === 19 || Number(err.code) === 23505) {
newArticle.slug = newArticle.slug + '-' + uuid().substr(-6)
await ctx.app.db('articles')
.update(humps.decamelizeKeys(
_.pick(
newArticle,
['title', 'slug', 'body', 'description', 'updatedAt']
)
))
.where({id: article.id})
} else {
throw err
}
}
if (fields.tagList && fields.tagList.length === 0) {
await ctx.app.db('articles_tags')
.del()
.where({article: article.id})
}
if (fields.tagList && fields.tagList.length > 0) {
if (_.difference(article.tagList).length || _.difference(fields.tagList).length) {
await ctx.app.db('articles_tags')
.del()
.where({article: article.id})
let tags = await Promise.all(
newArticle.tagList
.map(t => ({id: uuid(), name: t}))
.map(t => ctx.app.schemas.tag.validate(t, opts))
)
for (var i = 0; i < tags.length; i++) {
try {
await ctx.app.db('tags').insert(humps.decamelizeKeys(tags[i]))
} catch (err) {
if (Number(err.errno) !== 19 && Number(err.code) !== 23505) {
throw err
}
}
}
tags = await ctx.app.db('tags')
.select()
.whereIn('name', tags.map(t => t.name))
const relations = tags.map(t => ({
id: uuid(),
tag: t.id,
article: article.id
}))
await ctx.app.db('articles_tags').insert(relations)
}
}
newArticle.author = ctx.params.author
newArticle.favorited = article.favorited
ctx.body = {article: newArticle}
},
async del (ctx) {
const {article} = ctx.params
if (article.author.id !== ctx.state.user.id) {
ctx.throw(403, new ValidationError(['not owned by user'], '', 'article'))
}
await Promise.all([
ctx.app.db('favorites')
.del()
.where({user: ctx.state.user.id, article: article.id}),
ctx.app.db('articles_tags')
.del()
.where({article: article.id}),
ctx.app.db('articles')
.del()
.where({id: article.id})
])
ctx.body = {}
},
feed: {
async get (ctx) {
const {user} = ctx.state
const {offset, limit} = ctx.query
const followedQuery = ctx.app.db('followers')
.pluck('user')
.where({follower: user.id})
let [articles, [countRes]] = await Promise.all([
ctx.app.db('articles')
.select(
...getSelect('articles', 'article', articleFields),
...getSelect('users', 'author', userFields),
...getSelect('articles_tags', 'tag', ['id']),
...getSelect('tags', 'tag', ['id', 'name']),
'favorites.id as article_favorited'
)
.whereIn('articles.author', followedQuery)
.limit(limit)
.offset(offset)
.orderBy('articles.created_at', 'desc')
.leftJoin('users', 'articles.author', 'users.id')
.leftJoin('articles_tags', 'articles.id', 'articles_tags.article')
.leftJoin('tags', 'articles_tags.tag', 'tags.id')
.leftJoin('favorites', function () {
this.on('articles.id', '=', 'favorites.article')
.onIn('favorites.user', [user && user.id])
}),
ctx.app.db('articles').count().whereIn('author', followedQuery)
])
articles = joinJs
.map(articles, relationsMaps, 'articleMap', 'article_')
.map(a => {
a.favorited = Boolean(a.favorited)
a.tagList = a.tagList.map(t => t.name)
a.author.following = true
delete a.author.id
return a
})
let articlesCount = countRes.count || countRes['count(*)']
articlesCount = Number(articlesCount)
ctx.body = {articles, articlesCount}
}
},
favorite: {
async post (ctx) {
const {article} = ctx.params
if (article.favorited) {
ctx.body = {article: ctx.params.article}
return
}
await Promise.all([
ctx.app.db('favorites').insert({
id: uuid(),
user: ctx.state.user.id,
article: article.id
}),
ctx.app.db('articles')
.increment('favorites_count', 1)
.where({id: article.id})
])
article.favorited = true
article.favorites_count = Number(article.favorites_count) + 1
ctx.body = {article: ctx.params.article}
},
async del (ctx) {
const {article} = ctx.params
if (!article.favorited) {
ctx.body = {article: ctx.params.article}
return
}
await Promise.all([
ctx.app.db('favorites')
.del()
.where({user: ctx.state.user.id, article: article.id}),
ctx.app.db('articles')
.decrement('favorites_count', 1)
.where({id: article.id})
])
article.favorited = false
article.favorites_count = Number(article.favorites_count) - 1
ctx.body = {article: ctx.params.article}
}
},
comments
}

View File

@ -0,0 +1,84 @@
const humps = require('humps')
const uuid = require('uuid')
const _ = require('lodash')
const {getSelect} = require('lib/utils')
const {commentFields, userFields, relationsMaps} = require('lib/relations-map')
const joinJs = require('join-js').default
module.exports = {
async byComment (comment, ctx, next) {
if (!comment) {
ctx.throw(404)
}
comment = await ctx.app.db('comments').first().where({id: comment})
if (!comment) {
ctx.throw(404)
}
ctx.params.comment = comment
return next()
},
async get (ctx) {
const {user} = ctx.state
const {article} = ctx.params
let comments = await ctx.app.db('comments')
.select(
...getSelect('comments', 'comment', commentFields),
...getSelect('users', 'author', userFields),
'followers.id as author_following'
)
.where({article: article.id})
.leftJoin('users', 'comments.author', 'users.id')
.leftJoin('followers', function () {
this
.on('users.id', '=', 'followers.user')
.onIn('followers.follower', [user && user.id])
})
comments = joinJs
.map(comments, relationsMaps, 'commentMap', 'comment_')
.map(c => {
delete c.author.id
c.author.following = Boolean(c.author.following)
return c
})
ctx.body = {comments}
},
async post (ctx) {
const {body} = ctx.request
const {user} = ctx.state
const {article} = ctx.params
let {comment = {}} = body
const opts = {abortEarly: false}
comment.id = uuid()
comment.author = user.id
comment.article = article.id
comment = await ctx.app.schemas.comment.validate(comment, opts)
await ctx.app.db('comments').insert(humps.decamelizeKeys(comment))
comment.author = _.pick(user, ['username', 'bio', 'image', 'id'])
ctx.body = {comment}
},
async del (ctx) {
const {comment} = ctx.params
await ctx.app.db('comments').del().where({id: comment.id})
ctx.body = {}
}
}

9
src/controllers/index.js Normal file
View File

@ -0,0 +1,9 @@
const apiV1 = require('./apiV1-controller')
const products = require('./products-controller')
const login = require('./login-controller')
module.exports = {
apiV1,
products,
login
}

View File

@ -0,0 +1,54 @@
const { ValidationError } = require('lib/errors')
const { generateJWTforUser } = require('../lib/utils')
const { omit } = require('lodash')
module.exports = {
async postPopulate(ctx) {
const user = generateJWTforUser(ctx.state.user)
ctx.body = { user }
},
async postLogin(ctx) {
const { body } = ctx.request
if (!body.email || !body.password) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
/* let user = await ctx.app.db('users')
.first()
.where({ email: body.user.email })
if (!user) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
const isValid = await bcrypt.compare(body.user.password, user.password) */
let user = {
name: 'Test user',
email: 'test@test.sk'
};
const isValid = (body.email === user.email && body.password === 'test');
if (!isValid) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
user = generateJWTforUser(user)
ctx.body = { user: omit(user, ['password']) }
},
}

View File

@ -0,0 +1,51 @@
const _ = require('lodash')
module.exports = {
async byModule(moduleId, ctx, next) {
const { productId } = ctx.params;
if (!moduleId || !ctx.params.productId) {
ctx.throw(404)
}
module = await ctx.app.db('Module').first().where({
productId: productId,
moduleId: moduleId
})
if (!module) {
ctx.throw(404)
}
ctx.params.module = module
return next()
},
async get(ctx) {
const { product } = ctx.params
let modules = await ctx.app.db('Module')
.select(
'productId',
'moduleId',
'name'
)
.where({ productId: product.productId })
ctx.body = modules
},
async post(ctx) {
},
async put(ctx) {
},
async del(ctx) {
}
}

View File

@ -0,0 +1,76 @@
const modules = require('./modules-controller')
const { ValidationError } = require('lib/errors')
module.exports = {
async byProduct(productId, ctx, next) {
if (!productId) {
ctx.throw(404)
}
const product = await ctx.app.db('Product')
.first()
.where({ productId })
if (!product) {
ctx.throw(404)
}
ctx.params.product = product
return next()
},
async get(ctx) {
const { user } = ctx.state
const { offset, limit, tag, author, favorited } = ctx.query
let products = await ctx.app.db('Product')
.select('productId', 'name')
.limit(limit)
.offset(offset)
.orderBy('productId')
ctx.body = products
},
async post(ctx) {
const { body } = ctx.request
let product = body
const opts = { abortEarly: false }
product = await ctx.app.schemas.product.validate(product, opts)
await ctx.app.db('Product').insert(product)
ctx.body = product;
},
async put(ctx) {
const { product } = ctx.params
const { body } = ctx.request
let newProduct = body
const opts = { abortEarly: false }
newProduct = await ctx.app.schemas.product.validate(newProduct, opts)
await ctx.app.db('Product')
.update(newProduct)
.where('productId', product.productId)
ctx.body = newProduct;
},
async del(ctx) {
const { product } = ctx.params
await Promise.all([
ctx.app.db('product')
.del()
.where({ productId: product.productId }),
])
ctx.body = {}
},
modules
}

View File

@ -0,0 +1,105 @@
const _ = require('lodash')
const uuid = require('uuid')
const {getSelect} = require('lib/utils')
const {userFields, relationsMaps} = require('lib/relations-map')
const joinJs = require('join-js').default
module.exports = {
async byUsername (username, ctx, next) {
if (!username) {
ctx.throw(404)
}
const {user} = ctx.state
ctx.params.profile = await ctx.app.db('users')
.select(
...getSelect('users', 'profile', userFields),
'followers.id as profile_following'
)
.where({username})
.leftJoin('followers', function () {
this
.on('users.id', '=', 'followers.user')
.onIn('followers.follower', [user && user.id])
})
if (!ctx.params.profile || !ctx.params.profile.length) {
ctx.throw(404)
}
ctx.params.profile = joinJs.mapOne(
ctx.params.profile,
relationsMaps,
'userMap',
'profile_'
)
await next()
if (ctx.body.profile) {
ctx.body.profile = _.omit(ctx.body.profile, 'id')
ctx.body.profile.following = Boolean(ctx.body.profile.following)
}
},
async get (ctx) {
const {profile} = ctx.params
ctx.body = {profile}
},
follow: {
async post (ctx) {
const {profile} = ctx.params
const {user} = ctx.state
if (profile.following) {
ctx.body = {profile}
return
}
if (user.username !== profile.username) {
const follow = {
id: uuid(),
user: profile.id,
follower: user.id
}
try {
await ctx.app.db('followers').insert(follow)
} catch (err) {
if (Number(err.errno) !== 19 && Number(err.code) !== 23505) {
throw err
}
}
profile.following = true
}
ctx.body = {profile}
},
async del (ctx) {
const {profile} = ctx.params
const {user} = ctx.state
if (!profile.following) {
ctx.body = {profile}
return
}
await ctx.app.db('followers')
.where({user: profile.id, follower: user.id})
.del()
profile.following = false
ctx.body = {profile}
}
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
async get (ctx) {
const tags = await ctx.app.db('tags').pluck('name')
ctx.body = {tags}
}
}

View File

@ -0,0 +1,96 @@
const humps = require('humps')
const uuid = require('uuid')
const _ = require('lodash')
const bcrypt = require('bcrypt')
const {ValidationError} = require('lib/errors')
const {generateJWTforUser} = require('lib/utils')
module.exports = {
async get (ctx) {
const user = generateJWTforUser(ctx.state.user)
ctx.body = {user}
},
async post (ctx) {
const {body} = ctx.request
let {user = {}} = body
const opts = {abortEarly: false, context: {validatePassword: true}}
user.id = uuid()
user = await ctx.app.schemas.user.validate(user, opts)
user.password = await bcrypt.hash(user.password, 10)
await ctx.app.db('users').insert(humps.decamelizeKeys(user))
user = generateJWTforUser(user)
ctx.body = {user: _.omit(user, ['password'])}
},
async put (ctx) {
const {body} = ctx.request
let {user: fields = {}} = body
const opts = {abortEarly: false, context: {validatePassword: false}}
if (fields.password) {
opts.context.validatePassword = true
}
let user = Object.assign({}, ctx.state.user, fields)
user = await ctx.app.schemas.user.validate(user, opts)
if (fields.password) {
user.password = await bcrypt.hash(user.password, 10)
}
user.updatedAt = new Date().toISOString()
await ctx.app.db('users')
.where({id: user.id})
.update(humps.decamelizeKeys(user))
user = generateJWTforUser(user)
ctx.body = {user: _.omit(user, ['password'])}
},
async login (ctx) {
const {body} = ctx.request
if (!_.isObject(body.user) || !body.user.email || !body.user.password) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
let user = await ctx.app.db('users')
.first()
.where({email: body.user.email})
if (!user) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
const isValid = await bcrypt.compare(body.user.password, user.password)
if (!isValid) {
ctx.throw(
422,
new ValidationError(['is invalid'], '', 'email or password')
)
}
user = generateJWTforUser(user)
ctx.body = {user: _.omit(user, ['password'])}
}
}

View File

@ -1,9 +0,0 @@
const sqlite = require('sqlite')
const config = require('../../config')
async function connect() {
return sqlite.open(config.DATABASE_FILE, { verbose:true, promise:Promise })
}
exports.pool = connect()

View File

@ -1,71 +0,0 @@
// Load env vars from .env, always run this early
require('dotenv').config()
// 3rd party
const debug = require('debug')('app:index')
const Koa = require('koa')
const helmet = require('koa-helmet')
const compress = require('koa-compress')
const serveStatic = require('koa-better-static2')
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const bouncer = require('koa-bouncer')
const mount = require('koa-mount')
// 1st party
const config = require('../config')
const mw = require('./middleware')
// //////////////////////////////////////////////////////////
const app = new Koa()
app.poweredBy = false
app.proxy = config.TRUST_PROXY
// //////////////////////////////////////////////////////////
// Middleware
// //////////////////////////////////////////////////////////
app.use(helmet())
app.use(compress())
// TODO: You would set a high maxage on static assets if they had their hash in their filename.
// This project currently has no static asset build system setup.
app.use(mount('/static', serveStatic('static', { maxage: 0 })))
app.use(logger())
app.use(bodyParser())
/*var getRawBody = require('raw-body')
var contentType = require('content-type')
app.use(function* (next) {
console.log('data before ' + this.req.headers['content-length']);
this.text = yield getRawBody(this.req, {
length: this.req.headers['content-length'],
limit: '1mb',
encoding: contentType.parse(this.req).parameters.charset
})
console.log(this.text.toString());
console.log('data after');
yield next
}) */
app.use(mw.methodOverride()) // Must come after body parser
app.use(mw.removeTrailingSlash())
app.use(bouncer.middleware())
app.use(mw.handleBouncerValidationError()) // Must come after bouncer.middleware()
// //////////////////////////////////////////////////////////
// Routes
// //////////////////////////////////////////////////////////
app.use(require('./routes').routes())
// //////////////////////////////////////////////////////////
app.start = function (port = config.PORT) {
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`)
})
}
module.exports = app

3
src/lib/constants.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
HTTP: {}
}

13
src/lib/errors.js Normal file
View File

@ -0,0 +1,13 @@
const {ValidationError} = require('yup')
class UnauthorizedError extends Error {}
class ForbiddenError extends Error {}
class NotFoundError extends Error {}
class ServerError extends Error {}
module.exports = {
UnauthorizedError, // 401
ForbiddenError, // 403
NotFoundError, // 404
ValidationError, // 422
ServerError // 500
}

View File

@ -3,10 +3,10 @@ const zlib = require('zlib')
const denodeify = require('es6-denodeify')(Promise)
exports.generateLicenseFile = async function (activationId, appId, systemParams, licensedModules) {
exports.generateLicenseFile = async function (activationId, productId, systemParams, licensedModules) {
const activationData = {
activationId,
appId,
productId,
systemParams,
licensedModules
};

289
src/lib/licenseUtil.js Normal file
View File

@ -0,0 +1,289 @@
const uuid = require('uuid/v4')
const winston = require('winston')
const { generateLicenseFile } = require('./generateLicenseFile')
const { pickParams } = require("../config/supportedApps")
const findPreactivatedLicense = async function (db, productId, systemParams) {
winston.log('info', 'Looking for preactivated license for product id: ' + productId)
winston.log('info', 'Received system parameters', systemParams)
let counts = await Promise.all(Object.entries(systemParams).map(([param, val]) =>
db('PreactivationParam')
.first('paramId', 'paramValue')
.countDistinct('licenseNum as count')
.where({
productId,
paramId: param,
paramValue: val
})
.groupBy('paramId', 'paramValue')
.orderBy('paramId')
.orderBy('paramValue')
))
// If no licenses can be preactivated using any of given parameters
counts = counts
.filter((c) => (typeof c !== 'undefined' && c.count > 0))
const totalCount = counts
.reduce((a, b) => a.count + b.count, 0)
if (totalCount < 1) {
return null;
}
// Pick the most distinguishing parametere
const mostSignificantParam = counts
.sort((a, b) => (a.count - b.count))[0];
winston.log('info', 'Picked most distinguishing parameter: ', mostSignificantParam)
// Sanity check: Make sure that there are not too many licenses for the parameter
if (mostSignificantParam.count > 100) return null;
const licensesToCheck = await db('PreactivationParam')
.distinct('licenseNum')
//.select()
.where({
productId,
paramId: mostSignificantParam.paramId,
paramValue: mostSignificantParam.paramValue
})
.orderBy('licenseNum')
const availableLicenses = []
for (let license of licensesToCheck) {
const paramPairs = await db('PreactivationParam')
.select('paramId', 'paramValue')
.where({
productId,
licenseNum: license.licenseNum
})
const allMatch = true
for (let paramPair of paramPairs) {
if (systemParams[paramPair.paramId] !== paramPair.paramValue) {
allMatch = false
break
}
}
if (allMatch) {
availableLicenses.push(license.licenseNum)
}
}
return (availableLicenses.length > 0) ? availableLicenses[0] : null
}
exports.activate = async function (db, productId, licenseNum, systemParams) {
winston.log('info', 'Activating system for product id: [' + productId + '] and license num:' + licenseNum)
winston.log('info', 'Received system parameters', systemParams)
const activatedParams = await db
.from('ActiveActivationView as A0')
.innerJoin('ActivationParam as A1', 'A1.activationId', 'A0.activationId')
.select('A1.paramId', 'A1.paramValue', 'A1.flag')
.where('A0.productId', productId)
.andWhere('A0.licenseNum', licenseNum)
.orderBy('A1.paramId')
.orderBy('A1.paramValue')
if (activatedParams.length > 1) {
winston.log('warning', 'Already activated! Checking if system params still matches so we can reactivate', activatedParams)
for (let activatedParam of activatedParams) {
if (activatedParam.flag & 1 && systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'License already activated for different system' }
}
}
}
const activationId = uuid();
await db('Activation')
.update({
deactivatedOn: db.fn.now()
})
.where('licenseNum', licenseNum)
.whereNull('deactivatedOn')
await db('Activation').insert({
activationId,
productId,
licenseNum,
activatedOn: db.fn.now()
})
const pickedParams = pickParams(productId, systemParams);
await Promise.all(Object.keys(systemParams).map((d) => {
flag = (d in pickedParams) ? 1 : 0
return db('ActivationParam')
.insert({
activationId,
paramId: d,
paramValue: systemParams[d],
flag
})
}))
const licensedModulesRows = await db('LicensedModule')
.select('moduleId')
.where({
productId,
licenseNum
})
.orderBy('moduleId')
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, productId, pickedParams, licensedModules)).toString("base64")
}
}
exports.preactivate = async function(db, productId, systemParams) {
const preactivatedLicense = await findPreactivatedLicense(db, productId, systemParams)
if (preactivatedLicense === null) {
return {
success: false,
}
}
return await exports.activate(db, productId, preactivatedLicense, systemParams);
}
exports.check = async function (db, productId, systemParams, activationId, moduleVersions) {
const activatedParams = await db
.from('ActiveActivationView as A0')
.innerJoin('ActivationParam as A1', 'A1.activationId', 'A0.activationId')
.select('A1.paramId', 'A1.paramValue', 'A1.flag')
.where('A0.productId', productId)
.andWhere('A0.activationId', activationId)
.orderBy('A1.paramId')
.orderBy('A1.paramValue')
if (activatedParams.length < 1) {
return { success: false, reason: 'Not active' }
}
const checkLogId = uuid();
await db('CheckLog').insert({
checkLogId,
activationId,
checkedOn: db.fn.now()
})
await Promise.all(Object.keys(systemParams).map((d) =>
db('CheckLogParam').insert({
checkLogId,
paramId: d,
paramValue: systemParams[d]
})
))
await Promise.all(Object.keys(moduleVersions).map((d) =>
db('CheckLogVersion').insert({
checkLogId,
moduleId: d,
version: moduleVersions[d]
})
))
for (let activatedParam of activatedParams) {
if (activatedParam.flag & 1 && systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'Invalid activation' }
}
}
const lastModuleVersions = await db
.select('MU.moduleId', 'MU.version', 'MU.flag', 'MU.checksum', 'MU.updateUri', 'MU.instPath')
.from('LastModuleVersionView as LV')
.innerJoin('ModuleUpdate as MU', function () {
this.on('MU.productId', 'LV.productId')
.andOn('MU.moduleId', 'LV.moduleId')
.andOn('MU.version', 'LV.lastVersion')
})
.innerJoin('LicensedModule as LM', function () {
this.on('LM.productId', 'LV.productId')
.andOn('LM.moduleId', 'LV.moduleId')
})
.innerJoin('ActiveActivationView as A', function () {
this.on('A.productId', 'LM.productId')
.andOn('A.licenseNum', 'LM.licenseNum')
})
.where('LV.productId', productId)
.andWhere('A.activationId', activationId)
.orderBy('LV.moduleId')
let updateLicenseFile = (Object.keys(moduleVersions).length > lastModuleVersions.length);
const modulesToUpdate = [];
for (let m of lastModuleVersions) {
if (!(m.moduleId in moduleVersions)) {
if (Object.keys(moduleVersions).length) {
updateLicenseFile = true;
}
modulesToUpdate.push(m);
} else if(m.version > moduleVersions[m.moduleId]) {
modulesToUpdate.push({
...m,
currentVersion: moduleVersions[m.moduleId],
})
}
}
let updates = modulesToUpdate.filter((d) => !(d.flag & 1))
const lastIncrementalUpdates = modulesToUpdate.filter((d) => d.flag & 1)
if (lastIncrementalUpdates.length > 0) {
const incrementalUpdates = await db
.select('MU.moduleId', 'MU.version', 'MU.flag', 'MU.checksum', 'MU.updateUri', 'MU.instPath')
.from('ModuleUpdate as MU')
.innerJoin('LastFullVersionView as LFV', function () {
this.on('LFV.productId', 'MU.productId')
.andOn('LFV.moduleId', 'MU.moduleId')
.andOn('LFV.lastFullVersion', '<=', 'MU.version')
})
.where('MU.productId', productId)
.andWhere(function (queryBuilder) {
lastIncrementalUpdates.forEach(d => {
queryBuilder.orWhere(function (queryBuilder) {
queryBuilder.where('MU.moduleId', d.moduleId)
if (typeof (d.currentVersion) !== 'undefined') {
queryBuilder.andWhere('MU.version', '>', d.currentVersion)
}
})
})
})
.orderBy('MU.moduleId')
.orderBy('MU.version')
updates = updates.concat(incrementalUpdates)
}
if (updateLicenseFile) {
// Update license file as some modules received license since last activation
const licensedModulesRows = await db
.select('LM.moduleId')
.from('LicensedModule as LM')
.innerJoin('ActiveActivationView as A', function () {
this.on('A.productId', 'LM.productId')
.andOn('A.licenseNum', 'LM.licenseNum')
})
.where('A.productId', productId)
.andWhere('A.activationId', activationId)
.orderBy('LM.moduleId')
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
let activatedParamObj = {}
for (elm of activatedParams) {
activatedParamObj[elm.paramId] = elm.paramValue;
}
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, productId, activatedParamObj, licensedModules)).toString("base64"),
moduleUpdates: updates
}
}
return {
success: true,
moduleUpdates: updates
}
}

56
src/lib/relations-map.js Normal file
View File

@ -0,0 +1,56 @@
const userFields = ['id', 'image', 'bio', 'username']
const articleFields = [
'id',
'slug',
'title',
'body',
'description',
'favorites_count',
'created_at',
'updated_at'
]
const commentFields = [
'id',
'body',
'created_at',
'updated_at'
]
const relationsMaps = [
{
mapId: 'articleMap',
idProperty: 'id',
properties: [...articleFields, 'favorited'],
associations: [
{name: 'author', mapId: 'userMap', columnPrefix: 'author_'}
],
collections: [
{name: 'tagList', mapId: 'tagMap', columnPrefix: 'tag_'}
]
},
{
mapId: 'commentMap',
idProperty: 'id',
properties: [...commentFields],
associations: [
{name: 'author', mapId: 'userMap', columnPrefix: 'author_'}
]
},
{
mapId: 'userMap',
idProperty: 'id',
properties: [...userFields, 'following']
},
{
mapId: 'tagMap',
idProperty: 'id',
properties: ['id', 'name']
}
]
exports.relationsMaps = relationsMaps
exports.userFields = userFields
exports.articleFields = articleFields
exports.commentFields = commentFields

18
src/lib/utils.js Normal file
View File

@ -0,0 +1,18 @@
const {jwtSecret, jwtOptions} = require('config')
const jwt = require('jsonwebtoken')
const _ = require('lodash')
function generateJWTforUser (user = {}) {
return Object.assign({}, user, {
token: jwt.sign({
sub: _.pick(user, ['email'])
}, jwtSecret, jwtOptions)
})
}
function getSelect (table, prefix, fields) {
return fields.map(f => `${table}.${f} as ${prefix}_${f}`)
}
exports.generateJWTforUser = generateJWTforUser
exports.getSelect = getSelect

View File

@ -1,57 +0,0 @@
// 3rd
const debug = require('debug')('app:middleware')
const bouncer = require('koa-bouncer')
// 1st
const config = require('../config')
exports.methodOverride = function() {
return async (ctx, next) => {
if (typeof ctx.request.body === 'undefined') {
throw new Error(
'methodOverride middleware must be applied after the body is parsed and ctx.request.body is populated'
)
}
if (ctx.request.body && ctx.request.body._method) {
ctx.method = ctx.request.body._method.toUpperCase()
delete ctx.request.body._method
}
return next()
}
}
exports.removeTrailingSlash = function() {
return async (ctx, next) => {
if (ctx.path.length > 1 && ctx.path.endsWith('/')) {
ctx.redirect(ctx.path.slice(0, -1))
return
}
return next()
}
}
exports.handleBouncerValidationError = function() {
return async (ctx, next) => {
try {
await next()
} catch (err) {
if (err instanceof bouncer.ValidationError) {
ctx.flash = {
message: ['danger', err.message || 'Validation error'],
// CAVEAT: Max cookie size is 4096 bytes. If the user sent us a
// body that exceeds that (for example, a large message), then
// the cookie will not get set (silently).
// TODO: Consider using localStorage to persist request bodies
// so that it scales.
params: ctx.request.body,
bouncer: err.bouncer,
}
return ctx.redirect('back')
}
throw err
}
}
}

View File

@ -0,0 +1,8 @@
const {UnauthorizedError} = require('lib/errors')
module.exports = function (ctx, next) {
if (!ctx.state.user) {
ctx.throw(401, new UnauthorizedError())
}
return next()
}

View File

@ -0,0 +1,9 @@
const humps = require('humps')
const _ = require('lodash')
module.exports = async function (ctx, next) {
await next()
if (ctx.body && _.isObjectLike(ctx.body)) {
ctx.body = humps.camelizeKeys(ctx.body)
}
}

View File

@ -0,0 +1,32 @@
const config = require('config')
const fs = require('fs')
module.exports = function (app) {
if (config.db.client === 'sqlite3') {
try {
fs.mkdirSync(config.server.data)
} catch (err) {
if (err.code !== 'EEXIST') {
throw err
}
}
}
const db = require('knex')(config.db)
app.db = db
let promise
if (!config.env.isTest) {
app.migration = true
promise = db.migrate.latest()
.then(() => { app.migration = false }, console.error)
}
return async function (ctx, next) {
if (ctx.app.migration && promise) {
await promise
}
return next()
}
}

View File

@ -0,0 +1,83 @@
const errors = require('lib/errors')
let constants = require('lib/constants')
const _ = require('lodash')
const http = require('http')
Object.entries(http.STATUS_CODES).forEach(([key, value]) => {
constants.HTTP[key] = value
.toUpperCase()
.replace(/\s/igm, '_')
})
module.exports = async (ctx, next) => {
try {
await next()
if (Number(ctx.response.status) === 404 && !ctx.response.body) {
ctx.throw(404)
}
} catch (err) {
ctx.type = 'application/json'
if (!ctx.response.body) {
ctx.response.body = {errors: {}}
}
// ctx.app.emit('error', err, ctx);
console.error(err)
switch (true) {
case err instanceof errors.ValidationError:
ctx.body.errors = formatValidationError(err)
ctx.status = _.defaultTo(err.status, 422)
break
case err.code === 'SQLITE_CONSTRAINT': {
let path = 'unknown'
if (Number(err.errno) === 19) { // SQLITE3 UNIQUE
const idx = err.message.lastIndexOf('.')
if (idx !== -1) {
path = err.message.substring(idx + 1, err.message.length)
ctx.body.errors[path] = ['has already been taken']
}
}
ctx.status = _.defaultTo(err.status, 422)
break
}
case Number(err.code) === 23505: { // PG UNIQUE
let path = 'unknown'
const [key] = err.detail.match(/\(.+?\)/g)
if (key) {
path = key.substr(1, key.length - 2)
}
ctx.body.errors[path] = ['has already been taken']
ctx.status = _.defaultTo(err.status, 422)
break
}
default:
ctx.status = _.defaultTo(err.status, 500)
break
}
} finally {
if (ctx.body && !ctx.body.code) {
ctx.body.code = constants.HTTP[String(ctx.status)]
}
}
}
function formatValidationError (err) {
const result = {}
if (err.path) {
result[err.path] = [_.defaultTo(err.message, 'is not valid')]
}
if (err.inner && err.inner.length > 0) {
err.inner
.map(err => formatValidationError(err))
.reduce((prev, curr) => (Object.assign(prev, curr)), result)
}
return result
}

View File

@ -0,0 +1,21 @@
const koaJwt = require('koa-jwt')
const {jwtSecret} = require('config')
module.exports = koaJwt({
getToken (ctx, opts) {
const {authorization} = ctx.header
if (authorization && authorization.split(' ')[0] === 'Bearer') {
return authorization.split(' ')[1]
}
if (authorization && authorization.split(' ')[0] === 'Token') {
return authorization.split(' ')[1]
}
return null
},
secret: jwtSecret,
passthrough: true,
key: 'jwt'
})

View File

@ -0,0 +1,30 @@
const qs = require('qs')
const filters = ['tag', 'author', 'favorited']
module.exports = (ctx, next) => {
if (ctx.method !== 'GET') {
return next()
}
ctx.query = qs.parse(ctx.querystring)
const {query} = ctx
query.limit = parseInt(query.limit, 10) || 20
query.skip = query.offset = parseInt(query.offset, 10) || 0
if (query.page) {
query.page = parseInt(query.page, 10)
query.skip = query.offset = (query.page - 1) * query.limit
}
filters.forEach(f => {
if (!query[f] || Array.isArray(query[f])) return
if (query[f]) {
query[f] = [query[f]]
}
})
return next()
}

View File

@ -0,0 +1,28 @@
const {has} = require('lodash')
module.exports = async (ctx, next) => {
if (has(ctx, 'state.jwt.sub.email')) {
/* ctx.state.user = await ctx.app.db('users')
.first(
'id',
'email',
'username',
'image',
'bio',
'created_at',
'updated_at'
)
.where({id: ctx.state.jwt.sub.email}) */
let user = {
name: 'Test user',
email: 'test@test.sk'
};
if (ctx.state.jwt.sub.email == user.email) {
ctx.state.user = user;
};
}
return next()
}

View File

@ -0,0 +1,164 @@
exports.up = function (knex) {
const strActiveActivationView = knex.select('*').from('Activation').whereNull('deactivatedOn').toString()
const strLastModuleVersionView = knex
.select('productId', 'moduleId')
.max('version AS lastVersion')
.from('ModuleUpdate')
.groupBy('productId','moduleId')
.toString()
const strLastFullVersionView = knex
.select('productId', 'moduleId')
.max('version AS lastFullVersion')
.from('ModuleUpdate')
.whereRaw('?? & 1 <> 1', 'flag')
.groupBy('productId', 'moduleId')
.toString()
return knex.schema
.createTable('Product', function (table) {
table.string('productId', 20).unique().primary().notNullable()
table.string('name').notNullable()
table.timestamps(true, true)
})
.createTable('Module', function (table) {
table.string('productId', 20).notNullable()
table.string('moduleId', 40).notNullable()
table.string('name').notNullable()
table.timestamps(true, true)
table.primary(['productId', 'moduleId']);
table.foreign('productId')
.references('productId')
.on('Product')
})
.createTable('ModuleUpdate', function (table) {
table.string('productId', 20).notNullable()
table.string('moduleId', 40).notNullable()
table.string('version').notNullable()
table.string('checksum', 64).notNullable()
table.string('updateUri').notNullable()
table.string('instPath').notNullable()
table.integer('flag').notNullable().defaultTo(0)
table.timestamps(true, true)
table.primary(['productId', 'moduleId', 'version']);
table.foreign(['productId', 'moduleId'])
.references(['productId', 'moduleId'])
.on('Module')
})
.createTable('License', function (table) {
table.string('productId', 20).notNullable()
table.string('licenseNum', 24).notNullable()
table.string('customerId').notNullable()
table.timestamps(true, true)
table.primary(['productId', 'licenseNum']);
table.foreign('productId')
.references('productId')
.on('Product')
})
.createTable('LicensedModule', function (table) {
table.string('productId', 20).notNullable()
table.string('licenseNum', 24).notNullable()
table.string('moduleId', 40).notNullable()
table.timestamps(true, true)
table.primary(['productId', 'licenseNum', 'moduleId']);
table.foreign(['productId', 'licenseNum'])
.references(['productId', 'licenseNum'])
.on('License')
table.foreign(['productId', 'moduleId'])
.references(['productId', 'moduleId'])
.on('Module')
})
.createTable('PreactivationParam', function (table) {
table.string('productId', 20).notNullable()
table.string('licenseNum', 24).notNullable()
table.string('paramId', 40).notNullable()
table.string('paramValue', 40).notNullable()
table.string('paramOrig')
table.timestamps(true, true)
table.primary(['productId', 'licenseNum', 'paramId']);
table.foreign(['productId', 'licenseNum'])
.references(['productId', 'licenseNum'])
.on('License')
})
.createTable('Activation', function (table) {
table.uuid('activationId').unique().primary().notNullable()
table.string('productId', 20).notNullable()
table.string('licenseNum', 24).notNullable()
table.dateTime('activatedOn').notNullable().defaultTo(knex.fn.now())
table.dateTime('deactivatedOn')
table.foreign(['productId', 'licenseNum'])
.references(['productId', 'licenseNum'])
.on('License');
})
.createTable('ActivationParam', function (table) {
table.uuid('activationId').notNullable()
table.string('paramId', 40).notNullable()
table.string('paramValue', 40).notNullable()
table.integer('flag').notNullable().defaultTo(0)
table.primary(['activationId', 'paramId'])
table.foreign('activationId')
.references('activationId')
.on('Activation')
})
.createTable('CheckLog', function (table) {
table.uuid('checkLogId').notNullable()
table.uuid('activationId').notNullable()
table.dateTime('checkedOn').notNullable().defaultTo(knex.fn.now())
table.primary('checkLogId');
table.foreign('activationId')
.references('activationId')
.on('Activation')
})
.createTable('CheckLogParam', function (table) {
table.uuid('checkLogId').notNullable()
table.string('paramId', 40).notNullable()
table.string('paramValue', 40).notNullable()
table.integer('flag').notNullable().defaultTo(0)
table.primary(['checkLogId', 'paramId']);
table.foreign('checkLogId')
.references('checkLogId')
.on('CheckLog')
})
.createTable('CheckLogVersion', function (table) {
table.uuid('checkLogId').notNullable()
table.string('moduleId', 40).notNullable()
table.integer('version', 40).notNullable()
table.primary(['checkLogId', 'moduleId'])
table.foreign('checkLogId')
.references('checkLogId')
.on('CheckLog')
})
.raw('CREATE VIEW `ActiveActivationView` AS ' + strActiveActivationView)
.raw('CREATE VIEW `LastModuleVersionView` AS ' + strLastModuleVersionView)
.raw('CREATE VIEW `LastFullVersionView` AS ' + strLastFullVersionView)
}
// Dropping should happen in opposite order
exports.down = function (knex) {
return knex.schema
.raw('DROP VIEW `LastFullVersionView`')
.raw('DROP VIEW `LastModuleVersionView`')
.raw('DROP VIEW `ActiveActivationView`')
.dropTableIfExists('CheckLogVersion')
.dropTableIfExists('CheckLogParam')
.dropTableIfExists('CheckLog')
.dropTableIfExists('ActivationParam')
.dropTableIfExists('Activation')
.dropTableIfExists('PreactivationParam')
.dropTableIfExists('LicensedModule')
.dropTableIfExists('License')
.dropTableIfExists('ModuleUpdate')
.dropTableIfExists('Module')
.dropTableIfExists('Product')
}

View File

@ -0,0 +1,9 @@
const Router = require('koa-router')
const ctrl = require('controllers').apiV1
const router = new Router()
router.post('/v1/activate0', ctrl.preactivate)
router.post('/v1/activate', ctrl.activate)
router.post('/v1/check', ctrl.check)
module.exports = router.routes()

View File

@ -0,0 +1,26 @@
const Router = require('koa-router')
const ctrl = require('controllers').articles
const router = new Router()
const auth = require('middleware/auth-required-middleware')
router.param('slug', ctrl.bySlug)
router.param('comment', ctrl.comments.byComment)
router.get('/articles', ctrl.get)
router.post('/articles', auth, ctrl.post)
router.get('/articles/feed', auth, ctrl.feed.get)
router.get('/articles/:slug', ctrl.getOne)
router.put('/articles/:slug', auth, ctrl.put)
router.del('/articles/:slug', auth, ctrl.del)
router.post('/articles/:slug/favorite', auth, ctrl.favorite.post)
router.del('/articles/:slug/favorite', auth, ctrl.favorite.del)
router.get('/articles/:slug/comments', ctrl.comments.get)
router.post('/articles/:slug/comments', auth, ctrl.comments.post)
router.del('/articles/:slug/comments/:comment', auth, ctrl.comments.del)
module.exports = router.routes()

View File

@ -1,105 +1,18 @@
// 3rd party
const assert = require('better-assert')
const router = require('koa-router')()
const debug = require('debug')('app:routes:index')
// 1st party
const config = require('../../config')
const licenseUtil = require('../util/licenseUtil')
const Router = require('koa-router')
const router = new Router()
const api = new Router()
const backend = new Router()
//
// The index.js routes file is mostly a junk drawer for miscellaneous
// routes until it's accumulated enough routes to warrant a new
// routes/*.js module.
//
const apiV1 = require('./apiV1-router')
api.use(apiV1)
function checkSystemParams(systemParams) {
if (systemParams !== null && typeof systemParams === 'object') {
if (Object.keys(systemParams).length > 1) {
let valid = true
Object.entries(systemParams).forEach(([key, value]) => {
if (typeof key !== 'string' || typeof value !== 'string') {
valid = false
}
})
if (valid) {
return systemParams
}
}
}
throw new Error('Invalid parameters provided')
}
const products = require('./products-router')
const login = require('./login-router')
function checkLicenseNumber(licenseNumber) {
if (licenseNumber !== null && typeof licenseNumber === 'string') {
if (licenseNumber.length === 24) {
return licenseNumber
}
}
throw new Error('Invalid license number')
}
backend.use(products)
backend.use(login)
function checkActivationId(activationId) {
if (activationId !== null && typeof activationId === 'string') {
if (/^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/.test(activationId)) {
return activationId
}
}
throw new Error('Invalid activation id')
}
function checkAppId(appId) {
if (appId !== null && typeof appId === 'string') {
if (appId.length > 0) {
return (appId)
}
}
throw new Error('Invalid application id')
}
function isInt(value) {
return !isNaN(value) &&
parseInt(Number(value)) == value &&
!isNaN(parseInt(value, 10))
}
function checkModuleVersions(moduleVersions) {
if (moduleVersions !== null && typeof moduleVersions === 'object') {
let valid = true;
Object.entries(moduleVersions).forEach(([key, value]) => {
if (typeof key !== 'string' || !isInt(value)) {
valid = false;
}
})
if (valid) {
return moduleVersions;
}
}
throw new Error('Invalid parameters provided')
}
router.post('/api/v1/activate0', async ctx => {
console.log(ctx.request)
console.log(ctx.request.body)
ctx.body = await licenseUtil.preactivate(
checkAppId(ctx.request.body.appId),
checkSystemParams(ctx.request.body.systemParams)
)
})
router.post('/api/v1/activate', async ctx => {
ctx.body = await licenseUtil.activate(
checkAppId(ctx.request.body.appId),
checkLicenseNumber(ctx.request.body.licenseNumber),
checkSystemParams(ctx.request.body.systemParams)
)
})
router.post('/api/v1/check', async ctx => {
ctx.body = await licenseUtil.check(
checkSystemParams(ctx.request.body.systemParams),
checkActivationId(ctx.request.body.activationId),
checkModuleVersions(ctx.request.body.moduleVersions)
)
})
router.use('/api', api.routes())
router.use('/backend', backend.routes())
module.exports = router

View File

@ -0,0 +1,10 @@
const Router = require('koa-router')
const ctrl = require('controllers').login
const router = new Router()
const auth = require('middleware/auth-required-middleware')
router.post('/login', ctrl.postLogin)
router.post('/populate', auth, ctrl.postPopulate)
module.exports = router.routes()

View File

@ -0,0 +1,20 @@
const Router = require('koa-router')
const ctrl = require('controllers').products
const router = new Router()
const auth = require('middleware/auth-required-middleware')
router.param('productId', ctrl.byProduct)
router.param('moduleId', ctrl.modules.byModule)
router.get('/products', ctrl.get)
router.post('/products', auth, ctrl.post)
router.put('/products/:productId', auth, ctrl.put)
router.del('/products/:productId', auth, ctrl.del)
router.get('/products/:productId/modules', ctrl.modules.get)
router.post('/products/:productId/modules', auth, ctrl.modules.post)
router.put('/products/:productId/modules/:moduleId', auth, ctrl.modules.put)
router.del('/products/:productId/modules/:moduleId', auth, ctrl.modules.del)
module.exports = router.routes()

View File

@ -0,0 +1,13 @@
const Router = require('koa-router')
const ctrl = require('controllers').profiles
const router = new Router()
const auth = require('middleware/auth-required-middleware')
router.param('username', ctrl.byUsername)
router.get('/profiles/:username', ctrl.get)
router.post('/profiles/:username/follow', auth, ctrl.follow.post)
router.del('/profiles/:username/follow', auth, ctrl.follow.del)
module.exports = router.routes()

View File

@ -0,0 +1,7 @@
const Router = require('koa-router')
const ctrl = require('controllers').tags
const router = new Router()
router.get('/tags', ctrl.get)
module.exports = router.routes()

View File

@ -0,0 +1,13 @@
const Router = require('koa-router')
const ctrl = require('controllers').users
const router = new Router()
const auth = require('middleware/auth-required-middleware')
router.post('/users/login', ctrl.login)
router.post('/users', ctrl.post)
router.get('/user', auth, ctrl.get)
router.put('/user', auth, ctrl.put)
module.exports = router.routes()

View File

@ -0,0 +1,47 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const isUUID = require('validator/lib/isUUID')
const articleSchema = yup.object().shape({
id: yup.string()
.test({
name: 'id',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
author: yup.string()
.test({
name: 'user',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
slug: yup.string()
.trim(),
title: yup.string()
.required()
.trim(),
body: yup.string()
.required()
.trim(),
description: yup.string()
.required()
.trim(),
favoritesCount: yup.number()
.required()
.default(0),
tagList: yup.array()
.of(yup.string())
})
.noUnknown()
.concat(timeStampSchema)
module.exports = articleSchema

View File

@ -0,0 +1,36 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const isUUID = require('validator/lib/isUUID')
const commentSchema = yup.object().shape({
id: yup.string()
.test({
name: 'id',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
author: yup.string()
.test({
name: 'user',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
article: yup.string()
.test({
name: 'article',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
body: yup.string()
.required()
.trim()
})
.noUnknown()
.concat(timeStampSchema)
module.exports = commentSchema

9
src/schemas/index.js Normal file
View File

@ -0,0 +1,9 @@
const product = require('./product-schema')
const moduleSchema = require('./module-schema')
module.exports = function (app) {
app.schemas = {
product,
module: moduleSchema,
}
}

View File

@ -0,0 +1,20 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const moduleSchema = yup.object().shape({
productId: yup.string()
.required()
.trim(),
moduleId: yup.string()
.required()
.trim(),
name: yup.string()
.required()
.trim(),
})
.noUnknown()
.concat(timeStampSchema)
module.exports = moduleSchema

View File

@ -0,0 +1,17 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const productSchema = yup.object().shape({
productId: yup.string()
.required()
.trim(),
name: yup.string()
.required()
.trim(),
})
.noUnknown()
.concat(timeStampSchema)
module.exports = productSchema

23
src/schemas/tag-schema.js Normal file
View File

@ -0,0 +1,23 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const isUUID = require('validator/lib/isUUID')
const tagSchema = yup.object().shape({
id: yup.string()
.test({
name: 'id',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
name: yup.string()
.required()
.max(30)
.trim()
})
.noUnknown()
.concat(timeStampSchema)
module.exports = tagSchema

View File

@ -0,0 +1,37 @@
const yup = require('yup')
const isISO8601 = require('validator/lib/isISO8601').default
const timeStampsSchema = yup.object().shape({
createdAt: yup.string()
.required()
.test({
name: 'createdAt',
message: '${path} must be valid ISO8601 date', // eslint-disable-line
test: value => value ? isISO8601(new Date(value).toISOString()) : true
})
.transform(function (value) {
return this.isType(value) && value !== null
? new Date(value).toISOString()
: value
})
.default(() => new Date().toISOString()),
updatedAt: yup.string()
.required()
.test({
name: 'updatedAt',
message: '${path} must be valid ISO8601 date', // eslint-disable-line
test: value => value ? isISO8601(new Date(value).toISOString()) : true
})
.transform(function (value) {
return this.isType(value) && value !== null
? new Date(value).toISOString()
: value
})
.default(() => new Date().toISOString())
})
.noUnknown()
module.exports = timeStampsSchema

View File

@ -0,0 +1,45 @@
const yup = require('yup')
const timeStampSchema = require('./time-stamp-schema')
const isUUID = require('validator/lib/isUUID')
const userSchema = yup.object().shape({
id: yup.string()
.test({
name: 'id',
message: '${path} must be uuid', // eslint-disable-line
test: value => value ? isUUID(value) : true
}),
email: yup.string()
.required()
.email()
.lowercase()
.trim(),
password: yup.string()
.when('$validatePassword', {
is: true,
then: yup.string().required().min(8).max(30)
}),
username: yup.string()
.required()
.max(30)
.default('')
.trim(),
image: yup.string()
.url()
.default('')
.trim(),
bio: yup.string()
.default('')
.trim()
})
.noUnknown()
.concat(timeStampSchema)
module.exports = userSchema

22
src/seeds/01-products.js Normal file
View File

@ -0,0 +1,22 @@
const config = require('../config')
const getProducts = function () {
return [
{
productId: 'coc',
name: "Catalogue of Currencies"
}
]
}
exports.seed = async function (knex) {
const products = getProducts()
if (config.env.isProd) {
await knex('Product').whereIn('productId', products.map(d => d.productId)).del()
} else {
await knex('Product').del()
}
return Promise.all(products.map(a => knex('Product').insert(a)))
}

28
src/seeds/02-modules.js Normal file
View File

@ -0,0 +1,28 @@
const config = require('../config')
const getModules = function () {
return [
{
productId: 'coc',
moduleId: 'ccengine',
name: "Catalogue of Currencies application"
},
{
productId: 'coc',
moduleId: 'testdata',
name: "Catalogue of Currencies test data"
},
]
}
exports.seed = async function (knex) {
const modules = getModules()
if (config.env.isProd) {
await knex('Module').whereIn(['productId', 'moduleId'], modules.map(d => [d.productId, d.moduleId])).del()
} else {
await knex('Module').del()
}
return Promise.all(modules.map(a => knex('Module').insert(a)))
}

28
src/seeds/03-licenses.js Normal file
View File

@ -0,0 +1,28 @@
const config = require('../config')
const getLicenses = function () {
return [
{
productId: 'coc',
licenseNum: 'T3HZIFATHLN52I57HAGLV24R',
customerId: '123456'
},
{
productId: 'coc',
licenseNum: 'JK33BTBSBKSKV63YEVLMQMBZ',
customerId: '123456'
}
]
}
exports.seed = async function (knex) {
const licenses = getLicenses()
if (config.env.isProd) {
await knex('License').whereIn(['productId', 'licenseNum'], licenses.map(d => [d.productId, d.licenseNum])).del()
} else {
await knex('License').del()
}
return Promise.all(licenses.map(a => knex('License').insert(a)))
}

View File

@ -0,0 +1,38 @@
const config = require('../config')
const getLicensedModules = function () {
return [
{
productId: 'coc',
licenseNum: 'JK33BTBSBKSKV63YEVLMQMBZ',
moduleId: 'ccengine',
},
{
productId: 'coc',
moduleId: 'testdata',
licenseNum: 'JK33BTBSBKSKV63YEVLMQMBZ',
},
{
productId: 'coc',
licenseNum: 'T3HZIFATHLN52I57HAGLV24R',
moduleId: 'ccengine',
},
{
productId: 'coc',
moduleId: 'testdata',
licenseNum: 'T3HZIFATHLN52I57HAGLV24R',
}
]
}
exports.seed = async function (knex) {
const licensedModules = getLicensedModules()
if (config.env.isProd) {
await knex('LicensedModule').whereIn(['productId', 'moduleId', 'licenseNum'], licensedModules.map(d => [d.productId, d.moduleId, d.licenseNum])).del()
} else {
await knex('LicensedModule').del()
}
return Promise.all(licensedModules.map(a => knex('LicensedModule').insert(a)))
}

View File

@ -0,0 +1,25 @@
const config = require('../config')
const getPreactivationParams = function () {
return [
{
productId: 'coc',
licenseNum: 'T3HZIFATHLN52I57HAGLV24R',
paramId: 'biosSerialNum',
paramValue: '8690a8fb436070a9',
paramOrig: 'R80CW80'
},
]
}
exports.seed = async function (knex) {
const preactivationParams = getPreactivationParams()
if (config.env.isProd) {
await knex('PreactivationParam').whereIn(['productId', 'licenseNum', 'paramId'], preactivationParams.map(d => [d.productId, d.licenseNum, d.paramId])).del()
} else {
await knex('PreactivationParam').del()
}
return Promise.all(preactivationParams.map(a => knex('PreactivationParam').insert(a)))
}

View File

@ -0,0 +1,36 @@
const config = require('../config')
const getModuleUpdates = function () {
return [
{
productId: 'coc',
moduleId: 'testdata',
version: 2,
checksum: 'a515353daae35dc1b3e9e06e52b95a53690984cc3172bb4e6b44c6b516afa040',
updateUri: 'http://localhost:3000/static/testsite-v2-incremental.zip',
instPath: 'data',
flag: 1
},
{
productId: 'coc',
moduleId: 'testdata',
version: 1,
checksum: '6c878854d349752eceb0d52658e8838c2ae3cca53962c942a276e8944da25731',
updateUri: 'http://localhost:3000/static/testsite-v1.zip',
instPath: 'data',
flag: 0
}
]
}
exports.seed = async function (knex) {
const moduleUpdates = getModuleUpdates()
if (config.env.isProd) {
await knex('ModuleUpdate').whereIn(['productId', 'moduleId', 'version'], moduleUpdates.map(d => [d.productId, d.moduleId, d.version])).del()
} else {
await knex('ModuleUpdate').del()
}
return Promise.all(moduleUpdates.map(a => knex('ModuleUpdate').insert(a)))
}

View File

@ -1,243 +0,0 @@
const Guid = require('guid')
const config = require('../../config')
const { pool } = require('../db/pool')
const { generateLicenseFile } = require('.')
const { pickParams } = require("../../config/supportedApps")
let dbCache;
const getDb = async function () {
if (!dbCache) {
dbCache = await pool
dbCache.on("trace", console.log)
}
return dbCache
}
const findPreactivatedLicense = async function (appId, systemParams) {
console.log(appId);
console.log(systemParams);
const db = await getDb()
let counts = await Promise.all(Object.entries(systemParams).map(([param, val]) =>
db.get('SELECT paramId, paramValue, COUNT(DISTINCT(licenseNum)) as count' +
' FROM PreactivationParams' +
' WHERE appId = ? AND paramId = ? AND paramValue = ?' +
' GROUP BY paramId, paramValue' +
' ORDER BY paramId, paramValue',
appId, param, val)
))
// If no licenses can be preactivated using any of given parameters
counts = counts
.filter((c) => (typeof c !== 'undefined' && c.count > 0))
const totalCount = counts
.reduce((a, b) => a.count + b.count, 0)
if (totalCount < 1) {
return null;
}
// Pick the most distinguishing parametere
const mostSignificantParam = counts
.sort((a, b) => (a.count - b.count))[0];
// Sanity check: Make sure that there are not too many licenses for the parameter
if (mostSignificantParam > 100) return null;
const licensesToCheck = await db.all('SELECT licenseNum' +
' FROM PreactivationParams' +
' WHERE appId = ? AND paramId = ? AND paramValue = ?' +
' ORDER BY licenseNum',
appId, mostSignificantParam.paramId, mostSignificantParam.paramValue)
const availableLicenses = []
for (let license of licensesToCheck) {
const paramPairs = await db.all('SELECT paramId, paramValue FROM PreactivationParams WHERE licenseNum = ?', license.licenseNum)
const allMatch = true
for (let paramPair of paramPairs) {
if (systemParams[paramPair.paramId] !== paramPair.paramValue) {
allMatch = false
break
}
}
if (allMatch) {
availableLicenses.push(license.licenseNum)
}
}
return (availableLicenses.length > 0) ? availableLicenses[0] : null
}
exports.activate = async function (appId, licenseNum, systemParams) {
const db = await getDb()
const activatedParams = await db.all('SELECT A1.paramId, A1.paramValue' +
' FROM ActiveActivationView A0' +
' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' +
' WHERE A0.appId = ? AND A0.licenseNum = ?' +
' ORDER BY A1.paramId, A1.paramValue',
appId, licenseNum)
if (activatedParams.length > 1) {
// Already activated! Check if system params still matches so we can reactivate
for (let activatedParam of activatedParams) {
if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'License already activated for different system' }
}
}
}
const activationId = Guid.create();
await db.run('UPDATE Activation SET deactivatedOn = date(?) WHERE licenseNum = ? and deactivatedOn IS NULL', 'now', licenseNum)
await db.run('INSERT INTO Activation (activationId, appId, licenseNum, activatedOn)' +
' VALUES(?, ?, ?, date(?))', activationId.value, appId, licenseNum, 'now')
const pickedParams = pickParams(appId, systemParams);
await Promise.all(Object.keys(systemParams).map((d) => {
flag = (d.paramId in pickedParams) ? 1 : 0
return db.run('INSERT INTO ActivationParams (activationId, paramId, paramValue, flag)' +
' VALUES (?,?,?,?)', activationId.value, d, systemParams[d], flag)
}))
const licensedModulesRows = await db.all('SELECT moduleId' +
' FROM LicensedModule' +
' WHERE appId = ? AND licenseNum = ?' +
' ORDER BY moduleId',
appId, licenseNum)
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, appId, pickedParams, licensedModules)).toString("base64")
}
}
exports.preactivate = async function(appId, systemParams) {
const preactivatedLicense = await findPreactivatedLicense(appId, systemParams)
if (preactivatedLicense === null) {
return {
success: false,
}
}
return await exports.activate(appId, preactivatedLicense, systemParams);
}
exports.check = async function (systemParams, activationId, moduleVersions) {
const db = await getDb()
const activatedParams = await db.all('SELECT A0.appId, A1.paramId, A1.paramValue' +
' FROM ActiveActivationView A0' +
' INNER JOIN ActivationParams A1 on A1.activationId = A0.activationId' +
' WHERE A0.activationId = ?' +
' ORDER BY A1.paramId, A1.paramValue',
activationId)
if (activatedParams.length < 1) {
return { success: false, reason: 'Not active' }
}
const checkLogId = Guid.create().value;
await db.run('INSERT INTO CheckLog (checkLogId, activationId, checkedOn)' +
' VALUES(?, ?, date(?))', checkLogId, activationId, 'now')
await Promise.all(Object.keys(systemParams).map((d) =>
db.run('INSERT INTO CheckLogParams (checkLogId, paramId, paramValue)' +
' VALUES (?,?,?)', checkLogId, d, systemParams[d])))
await Promise.all(Object.keys(moduleVersions).map((d) =>
db.run('INSERT INTO CheckLogVersion (checkLogId, moduleId, version)' +
' VALUES (?,?,?)', checkLogId, d, moduleVersions[d])))
for (let activatedParam of activatedParams) {
if (systemParams[activatedParam.paramId] !== activatedParam.paramValue) {
return { success: false, reason: 'Invalid activation' }
}
}
const lastModuleVersions = await db.all(
'SELECT MU2.moduleId, MU2.version, MU2.flag, MU2.checksum, MU2.updateUri, MU2.instPath ' +
' FROM (SELECT MU.moduleId, MAX(MU.version) as lastVersion' +
' FROM ModuleUpdate MU' +
' INNER JOIN LicensedModule LM ON LM.moduleId = MU.moduleId' +
' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' +
' WHERE A.activationId = ?' +
' GROUP BY MU.moduleId) LV' +
' INNER JOIN ModuleUpdate MU2 ON MU2.moduleId = LV.moduleId AND MU2.version = LV.lastVersion' +
' ORDER BY LV.moduleId',
activationId);
let updateLicenseFile = (Object.keys(moduleVersions).length > lastModuleVersions.length);
const modulesToUpdate = [];
for (let m of lastModuleVersions) {
if (!(m.moduleId in moduleVersions)) {
if (Object.keys(moduleVersions).length) {
updateLicenseFile = true;
}
modulesToUpdate.push(m);
} else if(m.version > moduleVersions[m.moduleId]) {
modulesToUpdate.push({
...m,
currentVersion: moduleVersions[m.moduleId],
})
}
}
let updates = modulesToUpdate.filter((d) => !(d.flag & 1))
const lastIncrementalUpdates = modulesToUpdate.filter((d) => d.flag & 1)
if (lastIncrementalUpdates.length > 0) {
const conds = lastIncrementalUpdates.map((d) => {
let result = 'MU.moduleId = ?'
let params = [d.moduleId]
if (typeof (d.currentVersion) !== 'undefined') {
result += ' AND MU.version > ?'
params.push(d.currentVersion)
}
return {
cond: result,
params
}
})
const queryConds = conds.map((d) => d.cond).join(' OR ')
const queryParams = [].concat.apply([], conds.map((d) => d.params))
const incrementalUpdates = await db.all(
'SELECT MU.moduleId, MU.version, MU.flag, MU.checksum, MU.updateUri, MU.instPath ' +
' FROM ModuleUpdate MU' +
' INNER JOIN LastFullVersionView LFV ON LFV.moduleId = MU.moduleId AND LFV.lastFullVersion <= MU.version' +
' WHERE ' + queryConds +
' ORDER BY MU.moduleId, MU.version',
queryParams)
updates = updates.concat(incrementalUpdates)
}
if (updateLicenseFile) {
// Update license file as some modules received license since last activation
const licensedModulesRows = await db.all('SELECT LM.moduleId' +
' FROM LicensedModule LM' +
' INNER JOIN ActiveActivationView A ON A.appId = LM.appId AND A.licenseNum = LM.licenseNum' +
' WHERE A.activationId = ?' +
' ORDER BY LM.moduleId',
activationId)
const licensedModules = licensedModulesRows.map((d) => d.moduleId)
let appId = null;
let activatedParamObj = {}
for (elm of activatedParams) {
activatedParamObj[elm.paramId] = elm.paramValue;
appId = elm.appId;
}
return {
success: true,
licenseFile: (await generateLicenseFile(activationId, appId, activatedParamObj, licensedModules)).toString("base64"),
moduleUpdates: updates
}
}
return {
success: true,
moduleUpdates: updates
}
}