[refactor] Refactored app using koa
This commit is contained in:
parent
3a06e5d54f
commit
bbec2a1602
17
.eslintrc
17
.eslintrc
@ -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
7
.example-env
Normal 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
30
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -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" ]
|
||||
|
||||
209
README.md
209
README.md
@ -1,182 +1,65 @@
|
||||
# License server
|
||||
# 
|
||||
|
||||
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
|
||||
[](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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
53
knexfile.js
Normal file
53
knexfile.js
Normal 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'
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@ -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.-->
|
||||
|
||||
@ -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
|
||||
|
||||
8573
package-lock.json
generated
8573
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
)
|
||||
301
sql/schema.sql
301
sql/schema.sql
@ -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
69
src/app.js
Normal 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
41
src/bin/server.js
Normal 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
62
src/config/index.js
Normal 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
40
src/config/knexfile.js
Normal 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') }
|
||||
})
|
||||
|
||||
}
|
||||
102
src/controllers/apiV1-controller.js
Normal file
102
src/controllers/apiV1-controller.js
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
465
src/controllers/articles-controller.js
Normal file
465
src/controllers/articles-controller.js
Normal 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
|
||||
}
|
||||
84
src/controllers/comments-controller.js
Normal file
84
src/controllers/comments-controller.js
Normal 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
9
src/controllers/index.js
Normal 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
|
||||
}
|
||||
54
src/controllers/login-controller.js
Normal file
54
src/controllers/login-controller.js
Normal 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']) }
|
||||
},
|
||||
}
|
||||
51
src/controllers/modules-controller.js
Normal file
51
src/controllers/modules-controller.js
Normal 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) {
|
||||
}
|
||||
|
||||
}
|
||||
76
src/controllers/products-controller.js
Normal file
76
src/controllers/products-controller.js
Normal 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
|
||||
}
|
||||
105
src/controllers/profiles-controller.js
Normal file
105
src/controllers/profiles-controller.js
Normal 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}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
7
src/controllers/tags-controller.js
Normal file
7
src/controllers/tags-controller.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
async get (ctx) {
|
||||
const tags = await ctx.app.db('tags').pluck('name')
|
||||
|
||||
ctx.body = {tags}
|
||||
}
|
||||
}
|
||||
96
src/controllers/users-controller.js
Normal file
96
src/controllers/users-controller.js
Normal 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'])}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
71
src/index.js
71
src/index.js
@ -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
3
src/lib/constants.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
HTTP: {}
|
||||
}
|
||||
13
src/lib/errors.js
Normal file
13
src/lib/errors.js
Normal 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
|
||||
}
|
||||
@ -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
289
src/lib/licenseUtil.js
Normal 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
56
src/lib/relations-map.js
Normal 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
18
src/lib/utils.js
Normal 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
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/middleware/auth-required-middleware.js
Normal file
8
src/middleware/auth-required-middleware.js
Normal 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()
|
||||
}
|
||||
9
src/middleware/camelize-middleware.js
Normal file
9
src/middleware/camelize-middleware.js
Normal 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)
|
||||
}
|
||||
}
|
||||
32
src/middleware/db-middleware.js
Normal file
32
src/middleware/db-middleware.js
Normal 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()
|
||||
}
|
||||
}
|
||||
83
src/middleware/error-middleware.js
Normal file
83
src/middleware/error-middleware.js
Normal 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
|
||||
}
|
||||
21
src/middleware/jwt-middleware.js
Normal file
21
src/middleware/jwt-middleware.js
Normal 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'
|
||||
})
|
||||
30
src/middleware/pager-middleware.js
Normal file
30
src/middleware/pager-middleware.js
Normal 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()
|
||||
}
|
||||
28
src/middleware/user-middleware.js
Normal file
28
src/middleware/user-middleware.js
Normal 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()
|
||||
}
|
||||
164
src/migrations/20180428115300_init.js
Normal file
164
src/migrations/20180428115300_init.js
Normal 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')
|
||||
}
|
||||
9
src/routes/apiV1-router.js
Normal file
9
src/routes/apiV1-router.js
Normal 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()
|
||||
26
src/routes/articles-router.js
Normal file
26
src/routes/articles-router.js
Normal 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()
|
||||
@ -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
|
||||
|
||||
10
src/routes/login-router.js
Normal file
10
src/routes/login-router.js
Normal 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()
|
||||
20
src/routes/products-router.js
Normal file
20
src/routes/products-router.js
Normal 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()
|
||||
13
src/routes/profiles-router.js
Normal file
13
src/routes/profiles-router.js
Normal 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()
|
||||
7
src/routes/tags-router.js
Normal file
7
src/routes/tags-router.js
Normal 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()
|
||||
13
src/routes/users-router.js
Normal file
13
src/routes/users-router.js
Normal 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()
|
||||
47
src/schemas/article-schema.js
Normal file
47
src/schemas/article-schema.js
Normal 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
|
||||
36
src/schemas/comment-schema.js
Normal file
36
src/schemas/comment-schema.js
Normal 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
9
src/schemas/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
const product = require('./product-schema')
|
||||
const moduleSchema = require('./module-schema')
|
||||
|
||||
module.exports = function (app) {
|
||||
app.schemas = {
|
||||
product,
|
||||
module: moduleSchema,
|
||||
}
|
||||
}
|
||||
20
src/schemas/module-schema.js
Normal file
20
src/schemas/module-schema.js
Normal 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
|
||||
17
src/schemas/product-schema.js
Normal file
17
src/schemas/product-schema.js
Normal 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
23
src/schemas/tag-schema.js
Normal 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
|
||||
37
src/schemas/time-stamp-schema.js
Normal file
37
src/schemas/time-stamp-schema.js
Normal 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
|
||||
45
src/schemas/user-schema.js
Normal file
45
src/schemas/user-schema.js
Normal 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
22
src/seeds/01-products.js
Normal 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
28
src/seeds/02-modules.js
Normal 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
28
src/seeds/03-licenses.js
Normal 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)))
|
||||
}
|
||||
38
src/seeds/04-licensedModules.js
Normal file
38
src/seeds/04-licensedModules.js
Normal 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)))
|
||||
}
|
||||
25
src/seeds/05-preactivationParams.js
Normal file
25
src/seeds/05-preactivationParams.js
Normal 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)))
|
||||
}
|
||||
36
src/seeds/06-moduleUpdates.js
Normal file
36
src/seeds/06-moduleUpdates.js
Normal 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)))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user