[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
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# tmp
|
||||||
|
data
|
||||||
.env
|
.env
|
||||||
node_modules
|
|
||||||
.projectile
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
/.vs/
|
|
||||||
/bin/
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:9
|
FROM node:10
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@ -14,7 +14,7 @@ RUN npm install --only=production
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
VOLUME /usr/src/app/data
|
VOLUME /usr/src/app/data
|
||||||
RUN npm run reset-db
|
RUN npm run db:schema
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "npm", "start" ]
|
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
|
> ### 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.
|
||||||
client machines. The protocol is described below.
|
|
||||||
|
|
||||||
## 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
|
This codebase was created to demonstrate a fully fledged fullstack application built with **Koa.js + Knex** including CRUD operations, authentication, routing, pagination, and more.
|
||||||
if the computer has not been preactivated
|
|
||||||
|
|
||||||
```http
|
We've gone to great lengths to adhere to the **Koa.js + Knex** community styleguides & best practices.
|
||||||
POST /activate0 HTTP/1.1
|
|
||||||
|
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:
|
you can just copy `.example-env`
|
||||||
```json
|
|
||||||
{
|
|
||||||
"appId": "coc",
|
|
||||||
"systemParams": {
|
|
||||||
"biosSerialNum": "8690a8fb436070a9",
|
|
||||||
"computerUUID": "13cfc3b6f8f7fdd2",
|
|
||||||
"diskSerialNum": "63a58b9728485155",
|
|
||||||
"nicMac": "4b2856a1e9e8f43e",
|
|
||||||
"osId": "ec4fe2f3023d1f21"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The server checks if the system is preactivated by checking if any of the system parameters exists in `PreactivationParams` table.
|
## Variables description
|
||||||
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.
|
|
||||||
|
|
||||||
Example Response body:
|
`NODE_ENV` - specify env: development/production/test. `development` by default
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"licenseFile": "fG2HUWhE5kT10Ono3qgO/pzHNU1KAnUlEszz3B1pP5FdSQlIukg3P3xUgfHDQX1OBuAFH68WeXe2T0YP1dbjSsAyDJfpUltJnncwMMLOkfR3YbvyAVNmScgASLWwyQxQAVID6GOQ2weVNo3tAcbj2Ted7rx0HL36seSzyY5xuV/SnfJN6q5acqyJmibQcsrPQLZBvIb00Cy9KENnkVFVH70kC406pKxVZ3Ghqg8vTxgBK6sdbwqd7XrpupcQ4frwwm/NPesCIBYRG+6C/9oMroQoPZ+NEzOYQq2PADOZgOSvaMp4FUNe30IqNckobQO7N2TmW4BJDKVzhG5OYQRfs+xvzG0lF5gOBtmBhDf3yStgJj++jc2pNwe0rDSP2J+Yjs+V8wepyuCJ08cfZOh1feqnzmAarzAD80W8wRFd9U2lHDsyFt7Ke/3aPX454jxqITn/Wu6MK0paX8YAATWWAONyUsWTdA3UkJm45gNHJibcQcZLwKErZEPr2XBNy8nrImHRGFFtr3KC"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
`JWT_SECRET` - custom secret for generating jwt tokens. `secret` secret by default
|
||||||
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.
|
|
||||||
|
|
||||||
The client is supposed to send JSON request similar as in preactivation with just one parameter added `licenseNumber` which should be
|
`DB_CLIENT` - database to use. `pg` - postgress or `sqlite3`. `sqlite3` by default
|
||||||
uppercase without dashes so the server can directly query the database.
|
|
||||||
|
|
||||||
### Example
|
`DB_CONNECTION` - db connection string for `postgress` database.
|
||||||
```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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
# How it works
|
||||||
{
|
|
||||||
"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
|
|
||||||
|
|
||||||
|
> 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>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
<ProjectGuid>{8dfa872b-4368-4176-811a-6d3d9665e54b}</ProjectGuid>
|
<ProjectGuid>{cdb93689-38b1-497c-a17e-3fea4607a7e8}</ProjectGuid>
|
||||||
<ProjectHome />
|
<ProjectHome />
|
||||||
<ProjectView>ShowAllFiles</ProjectView>
|
<ProjectView>ShowAllFiles</ProjectView>
|
||||||
<StartupFile />
|
<StartupFile />
|
||||||
@ -12,40 +12,57 @@
|
|||||||
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
|
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
|
||||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
|
||||||
<ItemGroup>
|
<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="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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="backend" />
|
<Folder Include="src" />
|
||||||
<Folder Include="license-server" />
|
<Folder Include="src\bin" />
|
||||||
<Folder Include="sql\" />
|
<Folder Include="src\config" />
|
||||||
<Folder Include="src\" />
|
<Folder Include="src\controllers" />
|
||||||
<Folder Include="src\db\" />
|
<Folder Include="src\lib" />
|
||||||
<Folder Include="src\routes\" />
|
<Folder Include="src\middleware" />
|
||||||
<Folder Include="src\util\" />
|
<Folder Include="src\migrations" />
|
||||||
</ItemGroup>
|
<Folder Include="src\routes" />
|
||||||
<ItemGroup>
|
<Folder Include="src\schemas" />
|
||||||
<Compile Include="config.js">
|
<Folder Include="src\seeds" />
|
||||||
<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>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
<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.-->
|
<!--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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.27130.2036
|
VisualStudioVersion = 15.0.27428.2011
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -11,15 +11,15 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8DFA872B-4368-4176-811A-6D3D9665E54B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CDB93689-38B1-497C-A17E-3FEA4607A7E8}.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}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C030B8B7-C3CC-4C52-97CA-1C3852CB83D9}
|
SolutionGuid = {2D183C48-BF8D-4B4D-920B-C5C498E25DC1}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
8613
package-lock.json
generated
8613
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",
|
"version": "1.0.0",
|
||||||
"description": "License Server",
|
"description": "conduit on koa.js",
|
||||||
"main": "index.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": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.4",
|
"bcrypt": "^2.0.1",
|
||||||
|
"cross-env": "^5.1.4",
|
||||||
|
"date-fns": "^1.29.0",
|
||||||
"dotenv": "^5.0.1",
|
"dotenv": "^5.0.1",
|
||||||
"es6-denodeify": "^0.1.5",
|
"es6-denodeify": "^0.1.5",
|
||||||
"guid": "0.0.12",
|
"http-shutdown": "^1.2.0",
|
||||||
"koa": "^2.5.0",
|
"humps": "^2.0.1",
|
||||||
"koa-better-static2": "^1.0.2",
|
"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-bodyparser": "^4.2.0",
|
||||||
"koa-bouncer": "^6.0.4",
|
"koa-helmet": "^4.0.0",
|
||||||
"koa-compress": "^2.0.0",
|
"koa-jwt": "^3.3.1",
|
||||||
"koa-helmet": "^3.3.0",
|
"koa-logger": "^3.2.0",
|
||||||
"koa-logger": "^3.1.0",
|
"koa-response-time": "^2.0.0",
|
||||||
"koa-mount": "^3.0.0",
|
|
||||||
"koa-route": "^3.2.0",
|
|
||||||
"koa-router": "^7.4.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": {},
|
"devDependencies": {
|
||||||
"scripts": {
|
"faker": "^4.1.0",
|
||||||
"start": "node index.js",
|
"jest": "^22.4.3",
|
||||||
"reset-db": "node ./sql/reset-db",
|
"pre-commit": "^1.2.2",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"snazzy": "^7.1.1",
|
||||||
|
"standard": "^11.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"jest": {
|
||||||
"node": ">=9.x"
|
"testPathIgnorePatterns": [
|
||||||
},
|
"<rootDir>[/\\\\](docs|node_modules)[/\\\\]"
|
||||||
"author": "Peter Sykora",
|
],
|
||||||
"license": "ISC"
|
"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)
|
const denodeify = require('es6-denodeify')(Promise)
|
||||||
|
|
||||||
exports.generateLicenseFile = async function (activationId, appId, systemParams, licensedModules) {
|
exports.generateLicenseFile = async function (activationId, productId, systemParams, licensedModules) {
|
||||||
const activationData = {
|
const activationData = {
|
||||||
activationId,
|
activationId,
|
||||||
appId,
|
productId,
|
||||||
systemParams,
|
systemParams,
|
||||||
licensedModules
|
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 Router = require('koa-router')
|
||||||
const assert = require('better-assert')
|
const router = new Router()
|
||||||
const router = require('koa-router')()
|
const api = new Router()
|
||||||
const debug = require('debug')('app:routes:index')
|
const backend = new Router()
|
||||||
// 1st party
|
|
||||||
const config = require('../../config')
|
|
||||||
const licenseUtil = require('../util/licenseUtil')
|
|
||||||
|
|
||||||
//
|
const apiV1 = require('./apiV1-router')
|
||||||
// The index.js routes file is mostly a junk drawer for miscellaneous
|
api.use(apiV1)
|
||||||
// routes until it's accumulated enough routes to warrant a new
|
|
||||||
// routes/*.js module.
|
|
||||||
//
|
|
||||||
|
|
||||||
function checkSystemParams(systemParams) {
|
const products = require('./products-router')
|
||||||
if (systemParams !== null && typeof systemParams === 'object') {
|
const login = require('./login-router')
|
||||||
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) {
|
backend.use(products)
|
||||||
if (licenseNumber !== null && typeof licenseNumber === 'string') {
|
backend.use(login)
|
||||||
if (licenseNumber.length === 24) {
|
|
||||||
return licenseNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('Invalid license number')
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkActivationId(activationId) {
|
router.use('/api', api.routes())
|
||||||
if (activationId !== null && typeof activationId === 'string') {
|
router.use('/backend', backend.routes())
|
||||||
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)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
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