first commit
This commit is contained in:
commit
dc354cf586
37 changed files with 8225 additions and 0 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = crlf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
16
.env-example
Normal file
16
.env-example
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
APP_PORT=#internal port of the app, i.e. 2912
|
||||||
|
DB_HOST=#host of the database, i.e. wisski_cloud_landing_db
|
||||||
|
DB_PORT=#port of the database, i.e. 5432
|
||||||
|
EXPOSED_PORT=#external port of the app exposed to the host, i.e. 2912
|
||||||
|
LOG_LEVEL=#info, debug, warning, error...
|
||||||
|
MAIL_HOST=# Mail Server, i.e. "mail.your-server.de"
|
||||||
|
MAIL_PASSWORD=# Mail account password, i.e. "123456"
|
||||||
|
MAIL_PORT=# Mail Server Port, i.e. 587
|
||||||
|
MAIL_USER=# Mail account username, i.e. "wisski_user"
|
||||||
|
MAIL_SECURE=# true (TLS/STARTTLS) or false
|
||||||
|
NODE_ENV=#development, production
|
||||||
|
POSTGRES_DB=#name of the database
|
||||||
|
POSTGRES_PASSWORD=#password of the database
|
||||||
|
POSTGRES_USER=#user of the database
|
||||||
|
WEBSOCKET_TOKEN=#security token
|
||||||
|
WEBSOCKET_URL=#websocket url of wisski cloud
|
||||||
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
webpack.config.js
|
||||||
6
.eslintrc.cjs
Normal file
6
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": ["eslint:recommended", 'plugin:@typescript-eslint/recommended'],
|
||||||
|
"plugins": ['@typescript-eslint'],
|
||||||
|
root: true,
|
||||||
|
};
|
||||||
83
.gitignore
vendored
Normal file
83
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
app/*.log
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# settings
|
||||||
|
settings.php
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
mongodb
|
||||||
|
|
||||||
|
# lock file
|
||||||
|
app/package-lock.json
|
||||||
|
|
||||||
|
# volumes
|
||||||
|
drupal
|
||||||
|
postgres
|
||||||
|
|
||||||
|
# scrap
|
||||||
|
wisski_cloud_account_manager.tar.gz
|
||||||
|
|
||||||
3
.jshintrc
Normal file
3
.jshintrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"esversion": 11
|
||||||
|
}
|
||||||
10
.travis.yml
Normal file
10
.travis.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"language": "node_js",
|
||||||
|
"node_js": "10",
|
||||||
|
"services": [
|
||||||
|
"mongodb"
|
||||||
|
],
|
||||||
|
"script": [
|
||||||
|
"npm run test"
|
||||||
|
]
|
||||||
|
}
|
||||||
247
api-spec.yaml
Normal file
247
api-spec.yaml
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
openapi: "3.1.0"
|
||||||
|
info:
|
||||||
|
version: 1.0.0
|
||||||
|
title: WissKI Cloud API
|
||||||
|
summary: Handles account creation and validation requests.
|
||||||
|
contact:
|
||||||
|
name: Robert Nasarek
|
||||||
|
email: r.nasarek@gnm.de
|
||||||
|
licence:
|
||||||
|
name: AGPL 2
|
||||||
|
url: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://{host}:{port}/wisski-cloud-daemon/api/v1
|
||||||
|
description: Production server
|
||||||
|
variables:
|
||||||
|
host:
|
||||||
|
default: localhost
|
||||||
|
descriptions: Host name of the server
|
||||||
|
port:
|
||||||
|
default: 3000
|
||||||
|
descriptions: Port number of the server
|
||||||
|
- url: http://[host]:{port}/wisski-cloud-daemon/api/v1
|
||||||
|
description: Local server
|
||||||
|
variables:
|
||||||
|
host:
|
||||||
|
default: localhost
|
||||||
|
descriptions: Host name of the server
|
||||||
|
port:
|
||||||
|
default: 3000
|
||||||
|
descriptions: Port number of the server
|
||||||
|
paths:
|
||||||
|
/healthcheck:
|
||||||
|
get:
|
||||||
|
summary: Healthcheck of API
|
||||||
|
tags:
|
||||||
|
[ 'healthcheck' ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: success
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/account/all:
|
||||||
|
get:
|
||||||
|
summary: Show all users
|
||||||
|
tags:
|
||||||
|
[ 'accounts' ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
"$ref": "#/components/schemas/DbAccount"
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/account/:
|
||||||
|
post:
|
||||||
|
summary: Adds new account
|
||||||
|
tags:
|
||||||
|
[ 'add account' ]
|
||||||
|
requestBody:
|
||||||
|
description: "Account object that needs to be added"
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
"$ref": "#/components/schemas/FormAccount"
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
"$ref": "#/components/schemas/DbAccount"
|
||||||
|
"message": "Account added successfully."
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
/account/{id}:
|
||||||
|
delete:
|
||||||
|
summary: Delete account with id
|
||||||
|
tags:
|
||||||
|
[ 'delete account' ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: success
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
get:
|
||||||
|
summary: Show account with id
|
||||||
|
tags:
|
||||||
|
[ 'show account' ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: success
|
||||||
|
content :
|
||||||
|
"*/*":
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GetAccount"
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
put:
|
||||||
|
summary: Update account with id
|
||||||
|
tags:
|
||||||
|
[ 'update account' ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: success
|
||||||
|
'500':
|
||||||
|
description: unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
parameters:
|
||||||
|
name: id
|
||||||
|
in: path
|
||||||
|
description: "ID of account to show"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Error:
|
||||||
|
required:
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
DbAccount:
|
||||||
|
properties:
|
||||||
|
personName:
|
||||||
|
type: string
|
||||||
|
example: "Peter Peterson"
|
||||||
|
description: "Name of the person"
|
||||||
|
organisation:
|
||||||
|
type: string
|
||||||
|
example: "Example Organisation"
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
example: "peter@example.com"
|
||||||
|
description: "Email of the person"
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: "peter"
|
||||||
|
description: "Username of the person"
|
||||||
|
subdomain:
|
||||||
|
type: string
|
||||||
|
example: "my_instance"
|
||||||
|
description: "Subdomain of the person"
|
||||||
|
validationCode:
|
||||||
|
type: string
|
||||||
|
example: "B9s8uP1xlG9411MFj32bQEsBya6NeSrJ"
|
||||||
|
description: "Validation code of the person"
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
description: "Is the account valid?"
|
||||||
|
provisioned:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
description: "Is the account provisioned?"
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
example: "64e5c5dacbf0ce4fbfdec62a"
|
||||||
|
description: "ID of the account"
|
||||||
|
createdAt:
|
||||||
|
type: date
|
||||||
|
example: "2021-08-23T08:20:07.749Z"
|
||||||
|
description: "Creation date of the account"
|
||||||
|
updatedAt:
|
||||||
|
type: date
|
||||||
|
example: "2021-08-23T08:20:07.749Z"
|
||||||
|
description: "Update date of the account"
|
||||||
|
__v:
|
||||||
|
type: integer
|
||||||
|
example: 0
|
||||||
|
description: "Version of the account"
|
||||||
|
FormAccount:
|
||||||
|
properties:
|
||||||
|
personName:
|
||||||
|
type: string
|
||||||
|
example: "Peter Peterson"
|
||||||
|
description: "Name of the person"
|
||||||
|
organisation:
|
||||||
|
type: string
|
||||||
|
example: "Example Organisation"
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
example: "peter@example.com"
|
||||||
|
description: "Email of the person"
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: "peter"
|
||||||
|
description: "Username of the person"
|
||||||
|
subdomain:
|
||||||
|
type: string
|
||||||
|
example: "my_instance"
|
||||||
|
description: "Subdomain of the person"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: "123456789ABCDEFGHIJKLMOPQRSTUVWXYZ"
|
||||||
|
description: "Password of the person"
|
||||||
|
GetAccount:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
validationCode:
|
||||||
|
type: string
|
||||||
|
createdAt:
|
||||||
|
type: date
|
||||||
|
updatedAt:
|
||||||
|
type: data
|
||||||
6454
package-lock.json
generated
Normal file
6454
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
73
package.json
Normal file
73
package.json
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"name": "wisski_cloud_daemon",
|
||||||
|
"homepage": "https://github.com/owner/project#readme",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "API to handle provisions at the WissKI Cloud.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"watch:build": "npx webpack --mode=development",
|
||||||
|
"watch:server": "nodemon --watch dist/bundle.js --exec 'node dist/bundle.js'",
|
||||||
|
"development": "npm-run-all -p watch:build watch:server",
|
||||||
|
"production": "npm-run-all -p build start",
|
||||||
|
"build": "npx webpack --mode=production",
|
||||||
|
"start": "node ./dist/bundle.js",
|
||||||
|
"test": "mocha ./src/test/**/*.js --exit",
|
||||||
|
"lint": "eslint './src/**/*.js' --ignore-path .eslintignore"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"node.js",
|
||||||
|
"express.js",
|
||||||
|
"wisski",
|
||||||
|
"websocket"
|
||||||
|
],
|
||||||
|
"author": "Robert Nasarek",
|
||||||
|
"license": "AGPL 2",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"log4js": "^6.4.0",
|
||||||
|
"mariadb": "^3.2.2",
|
||||||
|
"mongoose": "^7.5.3",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"nodemailer": "^6.9.7",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sequelize": "^6.34.0",
|
||||||
|
"swagger-ui-express": "^5.0.0",
|
||||||
|
"webpack-node-externals": "^3.0.0",
|
||||||
|
"ws": "^8.13.0",
|
||||||
|
"yamljs": "^0.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.6",
|
||||||
|
"@types/cors": "^2.8.14",
|
||||||
|
"@types/ejs": "^3.1.3",
|
||||||
|
"@types/express": "^4.17.20",
|
||||||
|
"@types/log4js": "^2.3.5",
|
||||||
|
"@types/mocha": "^10.0.2",
|
||||||
|
"@types/mongoose": "^5.11.97",
|
||||||
|
"@types/morgan": "^1.9.6",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
"@types/supertest": "^2.0.13",
|
||||||
|
"@types/swagger-ui-express": "^4.1.4",
|
||||||
|
"@types/ws": "^8.5.5",
|
||||||
|
"@types/yamljs": "^0.2.32",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
|
"chai": "^4.0.2",
|
||||||
|
"eslint": "^8.50.0",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-loader": "^9.4.4",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"webpack": "^5.88.2",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/api/v1/apiV1.ts
Normal file
10
src/api/v1/apiV1.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import {Router} from "express";
|
||||||
|
import healthcheckRoutes from "./healthcheck/healthcheck.router";
|
||||||
|
import instanceRoutes from "./instance/instance.router";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
// Base routes for all API endpoints
|
||||||
|
router.use('/health-check', healthcheckRoutes);
|
||||||
|
router.use('/instance', instanceRoutes);
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
src/api/v1/healthcheck/healthcheck.controller.ts
Normal file
14
src/api/v1/healthcheck/healthcheck.controller.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import mailer from "../../../mailer";
|
||||||
|
|
||||||
|
const healthcheck = (req: Request, res: Response) => {
|
||||||
|
res.status(200).send('API is up and running!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const mailTest = (req: Request, res: Response) => {
|
||||||
|
let body = req.body;
|
||||||
|
mailer(body.to, body.credentials);
|
||||||
|
res.status(200).send('Mail sent!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export {healthcheck, mailTest};
|
||||||
11
src/api/v1/healthcheck/healthcheck.router.ts
Normal file
11
src/api/v1/healthcheck/healthcheck.router.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {Router} from "express";
|
||||||
|
const router = Router();
|
||||||
|
import {healthcheck, mailTest} from "./healthcheck.controller";
|
||||||
|
|
||||||
|
// Base routes for all API endpoints
|
||||||
|
router.get('/', healthcheck);
|
||||||
|
|
||||||
|
// Test route for sending mails
|
||||||
|
router.get('/mail-test', mailTest);
|
||||||
|
|
||||||
|
export default router;
|
||||||
128
src/api/v1/instance/instance.controller.ts
Normal file
128
src/api/v1/instance/instance.controller.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Import the Account model
|
||||||
|
import { getWisskiInstanceData } from '../../../databaseActions';
|
||||||
|
import Deleter from '../../../deleter';
|
||||||
|
import { appLogger, errorLogger } from '../../../logging/log';
|
||||||
|
import Provisioner from '../../../provisioner';
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
// Create a new provisioner
|
||||||
|
const provisioner = new Provisioner();
|
||||||
|
const deleter = new Deleter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for starting the provision.
|
||||||
|
*
|
||||||
|
* @param req Request
|
||||||
|
* @param res Response
|
||||||
|
*
|
||||||
|
* @returns {Promise<Response|undefined>}
|
||||||
|
*/
|
||||||
|
const proviseInstance = async (req: Request, res: Response): Promise<Response | undefined> => {
|
||||||
|
// Get the aid from the request query.
|
||||||
|
const { aid } = req.query;
|
||||||
|
appLogger.debug('Got aid from request:', aid);
|
||||||
|
// Query for instance data.
|
||||||
|
const wisskiInstance = await getWisskiInstanceData(parseInt(aid as string));
|
||||||
|
appLogger.debug('Got wisskiInstance from database:', wisskiInstance);
|
||||||
|
if (wisskiInstance === undefined) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'Account not found.',
|
||||||
|
data: {},
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If the instance is already provisioned, return a 200.
|
||||||
|
if (wisskiInstance.provisioned === 2) {
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'Account already provisioned.',
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Queue the instance for provisioning.
|
||||||
|
provisioner.queue(wisskiInstance);
|
||||||
|
|
||||||
|
// Return a 201: provision started.
|
||||||
|
return res.status(201).json({
|
||||||
|
message: 'Provision started. Please wait for the confirmation mail.',
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorLogger.error('provision failed for account with id:', aid, 'error:', error.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'provision failed for account with id:' + aid,
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteInstance = async (req: Request, res: Response): Promise<Response | undefined> => {
|
||||||
|
// Get the aid from the request query.
|
||||||
|
const { aid } = req.query;
|
||||||
|
|
||||||
|
// Query for instance data.
|
||||||
|
const wisskiInstance = await getWisskiInstanceData(parseInt(aid as string));
|
||||||
|
|
||||||
|
// If no instance exists, return a 200.
|
||||||
|
if (wisskiInstance === undefined) {
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'Account not found.',
|
||||||
|
data: {},
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// If the instance is already deleted, return a 200.
|
||||||
|
if (wisskiInstance.provisioned === 0) {
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'No instance to delete.',
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Queue the instance for deletion.
|
||||||
|
deleter.queue(wisskiInstance);
|
||||||
|
|
||||||
|
// Return a 201: delete started.
|
||||||
|
return res.status(201).json({
|
||||||
|
message: 'Delete started.',
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorLogger.error('delete failed for account with id:', aid, 'error:', error.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'delete failed for account with id:' + aid,
|
||||||
|
data: wisskiInstance,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the controller methods
|
||||||
|
export default {
|
||||||
|
deleteInstance,
|
||||||
|
proviseInstance
|
||||||
|
}
|
||||||
26
src/api/v1/instance/instance.router.ts
Normal file
26
src/api/v1/instance/instance.router.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Route configuration for account operations
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import instanceController from './instance.controller';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route for provision.
|
||||||
|
* Starts provision via websocket and set provision status.
|
||||||
|
*
|
||||||
|
* @param {number} aid
|
||||||
|
* The aid of the account to provision.
|
||||||
|
*/
|
||||||
|
router.put('/provision', instanceController.proviseInstance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route for delete.
|
||||||
|
* Starts delete via websocket, sets provision status
|
||||||
|
* and purge instance data. User and account data remain in database.
|
||||||
|
*
|
||||||
|
* @param {number} aid
|
||||||
|
* The aid of the account to delete.
|
||||||
|
*/
|
||||||
|
router.delete('/delete', instanceController.deleteInstance);
|
||||||
|
|
||||||
|
export default router;
|
||||||
24
src/app.middleware.ts
Normal file
24
src/app.middleware.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import cors from 'cors';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import YAML from 'yamljs';
|
||||||
|
import path from 'path';
|
||||||
|
import {appLogger} from './logging/log';
|
||||||
|
import {Express} from "express";
|
||||||
|
|
||||||
|
// Set middleware
|
||||||
|
appLogger.info('Setting up API middleware');
|
||||||
|
const apiSpecPath = path.resolve(__dirname, '..', 'api-spec.yaml'); //eslint-disable-line no-undef
|
||||||
|
const swaggerDocument = YAML.load(apiSpecPath);
|
||||||
|
|
||||||
|
// @TODO: Add API spec to swaggerDocument
|
||||||
|
export default function setMiddleware(app: Express) {
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(cors());
|
||||||
|
app.use('/wisski-cloud-daemon/api/v1/api-specs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||||
|
morgan.token('time', () => new Date().toISOString());
|
||||||
|
app.use(morgan('[:time] :remote-addr :method :url :status :res[content-length] :response-time ms'));
|
||||||
|
}
|
||||||
|
|
||||||
64
src/app.ts
Normal file
64
src/app.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import express, {NextFunction, Request, Response} from 'express';
|
||||||
|
const app = express();
|
||||||
|
import path from 'path';
|
||||||
|
import setMiddleware from './app.middleware';
|
||||||
|
import apiV1 from './api/v1/apiV1';
|
||||||
|
import { testDbConnection } from './db/connection';
|
||||||
|
|
||||||
|
// test db connection
|
||||||
|
testDbConnection();
|
||||||
|
|
||||||
|
// Express middleware
|
||||||
|
setMiddleware(app);
|
||||||
|
|
||||||
|
// Define EJS as Template Engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
// Define public folder
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
// Path to views
|
||||||
|
app.set('views', path.join(__dirname, 'templates'));
|
||||||
|
|
||||||
|
// Api base route
|
||||||
|
app.use('/wisski-cloud-daemon/api/v1/', apiV1);
|
||||||
|
|
||||||
|
app.get('/', (req: Request, res: Response) => {
|
||||||
|
res.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
<title>WissKI API Daemon</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WissKI API Daemon</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional routes
|
||||||
|
app.use('*', (req: Request, res: Response) => {
|
||||||
|
res.status(404).send('<!DOCTYPE html>\n' +
|
||||||
|
' <html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' <link rel="icon" href="/favicon.ico" type="image/x-icon">\n' +
|
||||||
|
' <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body>\n' +
|
||||||
|
' Endpoint not found!!!! \n' +
|
||||||
|
' </body>\n' +
|
||||||
|
' </html>');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.use((err :Error, req:Request, res: Response, next:NextFunction) => {
|
||||||
|
if (res.headersSent) {
|
||||||
|
return next(err)
|
||||||
|
} else {
|
||||||
|
res.status(500).send('Something went wrong!')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
12
src/bin/daemon.ts
Normal file
12
src/bin/daemon.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import app from "../app";
|
||||||
|
import {appConfig} from "../config/appConfig";
|
||||||
|
import {appLogger} from "../logging/log";
|
||||||
|
import {createServer} from "http";
|
||||||
|
// get port from config
|
||||||
|
const port = appConfig.port;
|
||||||
|
const server = createServer(app);
|
||||||
|
|
||||||
|
// start server
|
||||||
|
server.listen(port, () => {
|
||||||
|
appLogger.info(`Server is running on port ${port}`);
|
||||||
|
});
|
||||||
65
src/config/appConfig.ts
Normal file
65
src/config/appConfig.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {appLogger} from "../logging/log";
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const envPath = '.env';
|
||||||
|
require('dotenv').config({ path: envPath });
|
||||||
|
|
||||||
|
if (
|
||||||
|
!process.env.APP_PORT ||
|
||||||
|
!process.env.DB_HOST ||
|
||||||
|
!process.env.DB_PORT ||
|
||||||
|
!process.env.EXPOSED_PORT ||
|
||||||
|
!process.env.LOG_LEVEL ||
|
||||||
|
!process.env.MAIL_HOST ||
|
||||||
|
!process.env.MAIL_PASSWORD ||
|
||||||
|
!process.env.MAIL_PORT ||
|
||||||
|
!process.env.MAIL_SECURE ||
|
||||||
|
!process.env.MAIL_USER ||
|
||||||
|
!process.env.POSTGRES_USER ||
|
||||||
|
!process.env.POSTGRES_PASSWORD ||
|
||||||
|
!process.env.POSTGRES_DB ||
|
||||||
|
!process.env.WEBSOCKET_TOKEN ||
|
||||||
|
!process.env.WEBSOCKET_URL
|
||||||
|
) {
|
||||||
|
!process.env.WEBSOCKET_URL ||
|
||||||
|
appLogger.warn('Some environment variables are missing. Please check your .env file.');
|
||||||
|
}
|
||||||
|
// Port configuration for the application
|
||||||
|
export const appConfig: { port: number, websocketToken: string, websocketUrl: string } = {
|
||||||
|
port: process.env.APP_PORT ? parseInt(process.env.APP_PORT, 10) : 2912,
|
||||||
|
websocketToken: process.env.WEBSOCKET_TOKEN || 'token',
|
||||||
|
websocketUrl: process.env.WEBSOCKET_URL || 'ws://localhost:2912/'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Database configuration for the application
|
||||||
|
export const dbConfig: {host: string, user: string, password: string, database: string } = {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.POSTGRES_USER || 'root',
|
||||||
|
password: process.env.POSTGRES_PASSWORD || 'password',
|
||||||
|
database: process.env.POSTGRES_DB || 'db'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logging configuration for the application
|
||||||
|
export const logConfig: {level: string} = {
|
||||||
|
level: process.env.LOG_LEVEL || 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mail configuration for the application
|
||||||
|
export const mailConfig: {
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
secure: boolean,
|
||||||
|
auth:
|
||||||
|
{
|
||||||
|
user: string,
|
||||||
|
pass: string
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
host: process.env.MAIL_HOST || 'mail.your-server.de',
|
||||||
|
port: process.env.MAIL_PORT ? parseInt(process.env.MAIL_PORT, 10) : 25,
|
||||||
|
secure: process.env.MAIL_SECURE ? process.env.MAIL_SECURE === 'true' : true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.MAIL_USER || 'user',
|
||||||
|
pass: process.env.MAIL_PASSWORD || 'pass'
|
||||||
|
}
|
||||||
|
};
|
||||||
66
src/databaseActions.ts
Normal file
66
src/databaseActions.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Import the Account model
|
||||||
|
import { appLogger, errorLogger } from './logging/log';
|
||||||
|
import Account, { initAccount } from './models/account';
|
||||||
|
import User, { initUser } from './models/user';
|
||||||
|
import { sequelize } from './db/connection';
|
||||||
|
import WisskiInstanceModel from './models/wisskiInstance';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for starting the provision.
|
||||||
|
*
|
||||||
|
* @param aid number
|
||||||
|
*
|
||||||
|
* @returns {Promise<WisskiInstanceModel|undefined>}
|
||||||
|
*/
|
||||||
|
const getWisskiInstanceData = async (aid: number): Promise <WisskiInstanceModel | undefined>=> {
|
||||||
|
try {
|
||||||
|
// Define your User model
|
||||||
|
const User = initUser(sequelize);
|
||||||
|
|
||||||
|
// Define your WisskiCloudAccountManagerAccounts model
|
||||||
|
const Account = initAccount(sequelize);
|
||||||
|
|
||||||
|
// Define associations
|
||||||
|
User.hasOne(Account, { foreignKey: 'uid' });
|
||||||
|
Account.belongsTo(User, { foreignKey: 'uid' });
|
||||||
|
|
||||||
|
// Find the user and join account with the given aid.
|
||||||
|
const user = await User.findOne({
|
||||||
|
include: {
|
||||||
|
model: Account,
|
||||||
|
where: { aid: aid }
|
||||||
|
}
|
||||||
|
}) as User & { Account: Account };
|
||||||
|
|
||||||
|
appLogger.debug('Got user from database:', user);
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
errorLogger.error('Account with aid:' + aid + ' not found.');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create instance data object with the data from the user and account.
|
||||||
|
const wisskiInstance: WisskiInstanceModel = {
|
||||||
|
aid: user.Account.dataValues.aid,
|
||||||
|
uid: user.dataValues.uid,
|
||||||
|
status: user.dataValues.status,
|
||||||
|
name: user.dataValues.name,
|
||||||
|
mail: user.dataValues.mail,
|
||||||
|
subdomain: user.Account.dataValues.subdomain,
|
||||||
|
validation_code: user.Account.dataValues.validation_code,
|
||||||
|
person_name: user.Account.dataValues.person_name,
|
||||||
|
organisation: user.Account.dataValues.organisation,
|
||||||
|
provisioned: user.Account.dataValues.provisioned
|
||||||
|
}
|
||||||
|
return wisskiInstance;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorLogger.error(error.message);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Export the controller methods
|
||||||
|
export {
|
||||||
|
getWisskiInstanceData
|
||||||
|
}
|
||||||
20
src/db/connection.ts
Normal file
20
src/db/connection.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Sequelize } from 'sequelize';
|
||||||
|
import { dbConfig, logConfig } from "../config/appConfig";
|
||||||
|
import { appLogger, errorLogger } from "../logging/log";
|
||||||
|
|
||||||
|
// create a Sequelize instance
|
||||||
|
export const sequelize = new Sequelize(dbConfig.database, dbConfig.user, dbConfig.password, {
|
||||||
|
host: dbConfig.host,
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: logConfig.level == 'debug' ? msg => appLogger.debug(msg) : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// test db connection
|
||||||
|
export const testDbConnection = async () => {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
appLogger.info('Successfully connected to database: ' + dbConfig.host);
|
||||||
|
} catch (error) {
|
||||||
|
errorLogger.error('Failed to connect to database: ' + dbConfig.host + ' error: ' + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/deleter.ts
Normal file
87
src/deleter.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import Account, { initAccount } from "./models/account";
|
||||||
|
import User, { initUser } from "./models/user";
|
||||||
|
import { purgeInstance } from "./websocketHandler";
|
||||||
|
import { errorLogger, websocketLogger } from "./logging/log";
|
||||||
|
import { sequelize } from "./db/connection";
|
||||||
|
import WisskiInstanceModel from "./models/wisskiInstance";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the event that is emitted when a provision request is queued.
|
||||||
|
*/
|
||||||
|
const PROVISION_EVENT = 'provision';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that handles provisioning of instances.
|
||||||
|
*/
|
||||||
|
export default class Deleter {
|
||||||
|
private emitter: EventEmitter;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.emitter.on(PROVISION_EVENT, async (wisskiInstance: WisskiInstanceModel) => {
|
||||||
|
// Define your User model
|
||||||
|
const User = initUser(sequelize);
|
||||||
|
|
||||||
|
// Define your WisskiCloudAccountManagerAccounts model
|
||||||
|
const Account = initAccount(sequelize);
|
||||||
|
|
||||||
|
// Define associations
|
||||||
|
User.hasOne(Account, { foreignKey: 'uid' });
|
||||||
|
Account.belongsTo(User, { foreignKey: 'uid' });
|
||||||
|
const account = await Account.findOne({
|
||||||
|
include: {
|
||||||
|
model: User,
|
||||||
|
where: { uid: wisskiInstance.uid }
|
||||||
|
}
|
||||||
|
}) as Account & { User: User };
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
errorLogger.error('account not found for account id:', wisskiInstance.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
account.set('provisioned', 3);
|
||||||
|
await account.save()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.doDelete(wisskiInstance);
|
||||||
|
websocketLogger.info('Deletion succeeded for subdomain:', wisskiInstance.subdomain);
|
||||||
|
account.set('provisioned', 0);
|
||||||
|
await account.save();
|
||||||
|
return result
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
websocketLogger.error('Delete failed for subdomain:', wisskiInstance.subdomain);
|
||||||
|
errorLogger.error('Delete failed for subdomain:', wisskiInstance.subdomain, 'error: ', error);
|
||||||
|
account.set('provisioned', 3);
|
||||||
|
await account.save();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do the actual provision.
|
||||||
|
*
|
||||||
|
* @param WisskiInstanceModel wisskiInstance
|
||||||
|
* The subdomain of the instance to provision.
|
||||||
|
*/
|
||||||
|
async doDelete(wisskiInstance: WisskiInstanceModel) {
|
||||||
|
websocketLogger.info("starting to delete instance " + wisskiInstance.subdomain + ".wisski.cloud.")
|
||||||
|
const status = await purgeInstance(wisskiInstance.subdomain);
|
||||||
|
if (status.success) {
|
||||||
|
return ('success')
|
||||||
|
} else {
|
||||||
|
return status.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WisskiInstanceModel} wisskiInstance
|
||||||
|
*/
|
||||||
|
queue(wisskiInstance: WisskiInstanceModel) {
|
||||||
|
websocketLogger.info(`queueing subdomain: ${wisskiInstance.subdomain} for account: ${wisskiInstance.name}`);
|
||||||
|
this.emitter.emit(PROVISION_EVENT, wisskiInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
38
src/logging/log.ts
Normal file
38
src/logging/log.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import log4js from "log4js";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { logConfig } from "../config/appConfig";
|
||||||
|
// Define the path.
|
||||||
|
const logDirectory = path.join(__dirname, '../logs');
|
||||||
|
|
||||||
|
// check if it exits
|
||||||
|
if (!fs.existsSync(logDirectory)) {
|
||||||
|
fs.mkdirSync(logDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Log4JS configuration
|
||||||
|
log4js.configure({
|
||||||
|
appenders: {
|
||||||
|
app: { type: 'file', filename: path.join(logDirectory, 'app.log') },
|
||||||
|
console: { type: 'console' },
|
||||||
|
error: { type: 'file', filename: path.join(logDirectory, 'error.log') },
|
||||||
|
websocket: { type: 'file', filename: path.join(logDirectory, 'websocket.log') },
|
||||||
|
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
default: { appenders: ['app','console'], level: logConfig.level ?? 'info'},
|
||||||
|
error: { appenders: ['error', 'console'], level: 'error'},
|
||||||
|
websocket: {appenders: ['websocket', 'console'], level: 'info'},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const appLogger = log4js.getLogger('default');
|
||||||
|
const errorLogger = log4js.getLogger('error');
|
||||||
|
const websocketLogger = log4js.getLogger('websocket');
|
||||||
|
|
||||||
|
appLogger.info(`Logging level set to: ${logConfig.level}. ${logConfig.level == 'debug' ? 'THIS IS A SECURITY RISK! DELETE LOG WHEN YOU ARE FINISHED!' : ''}`);
|
||||||
|
|
||||||
|
export {
|
||||||
|
appLogger,
|
||||||
|
errorLogger,
|
||||||
|
websocketLogger};
|
||||||
83
src/mailer.ts
Normal file
83
src/mailer.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import { mailConfig } from "./config/appConfig";
|
||||||
|
import { appLogger, errorLogger } from "./logging/log";
|
||||||
|
import ejs from "ejs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: mailConfig.host,
|
||||||
|
port: mailConfig.port,
|
||||||
|
secure: mailConfig.secure,
|
||||||
|
auth: mailConfig.auth,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credentials interface.
|
||||||
|
*/
|
||||||
|
interface Credentials {
|
||||||
|
domain: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send mail with credentials to the user.
|
||||||
|
*
|
||||||
|
* @param to string
|
||||||
|
* The email address of the user.
|
||||||
|
* @param credentials Credentials
|
||||||
|
* The credentials of the user.
|
||||||
|
*/
|
||||||
|
async function mailer(to: string, credentials: Credentials) {
|
||||||
|
async function sendMailAndHandleResponse(to: string, data: string, credentials: any) {
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `"WissKI Cloud" <${mailConfig.auth.user}>`, // sender address
|
||||||
|
to: to, // list of receivers
|
||||||
|
subject: "Your WissKI Cloud Instance Credentials", // Subject line
|
||||||
|
html: data, // html body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!info.messageId) {
|
||||||
|
errorLogger.error(`Could not send message concerning ${credentials.domain} to ${to}.`);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Message not sent.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogger.info(`Sent message with credentials of ${credentials.domain} to ${to}.`);
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
message: 'Message sent: ' + info.messageId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (credentials.status === 'success') {
|
||||||
|
const mailTemplate = path.join(__dirname, 'templates', 'credentialMail.ejs');
|
||||||
|
ejs.renderFile(mailTemplate, { credentials }, async function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
errorLogger.error(`Could not render template for ${credentials.domain}. ${err}`);
|
||||||
|
} else {
|
||||||
|
return await sendMailAndHandleResponse(to, data, credentials);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const mailTemplate = path.join(__dirname, 'templates', 'errorMail.ejs');
|
||||||
|
ejs.renderFile(mailTemplate, { credentials }, async function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
errorLogger.error(`Could not render template for ${credentials.domain}. ${err}`);
|
||||||
|
} else {
|
||||||
|
return await sendMailAndHandleResponse(to, data, credentials);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
appLogger.error(`Could not send message concerning ${credentials.domain} to ${to}. ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mailer;
|
||||||
46
src/models/account.ts
Normal file
46
src/models/account.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { DataTypes, Model, Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
export default class Account extends Model {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initAccount(sequelize: Sequelize) {
|
||||||
|
Account.init({
|
||||||
|
aid: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
uid: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
},
|
||||||
|
person_name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
organisation: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
subdomain: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
validation_code: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
provisioned: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: "wisski_cloud_account_manager_accounts",
|
||||||
|
timestamps: false,
|
||||||
|
sequelize, // passing the `sequelize` instance is required
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return Account;
|
||||||
|
}
|
||||||
33
src/models/user.ts
Normal file
33
src/models/user.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { DataTypes, Model, Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
export default class User extends Model {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initUser(sequelize: Sequelize) {
|
||||||
|
User.init({
|
||||||
|
uid: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: false,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.SMALLINT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING(60),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
mail: {
|
||||||
|
type: DataTypes.STRING(254),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
tableName: "users_field_data",
|
||||||
|
timestamps: false,
|
||||||
|
sequelize, // passing the `sequelize` instance is required
|
||||||
|
});
|
||||||
|
|
||||||
|
return User;
|
||||||
|
}
|
||||||
16
src/models/wisskiInstance.ts
Normal file
16
src/models/wisskiInstance.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the wisski instance data.
|
||||||
|
*/
|
||||||
|
export default interface WisskiInstanceModel {
|
||||||
|
aid: number; // Account id, same as in user_fields_data.
|
||||||
|
mail: string; // User email.
|
||||||
|
name: string; // User name.
|
||||||
|
organisation: string|undefined; // User organisation.
|
||||||
|
person_name: string; // Real name of the user.
|
||||||
|
provisioned: number; // 0 = not provisioned, 1 = provisioning, 2 = provisioned, 3 = failed.
|
||||||
|
status: number; // User status 0 = not valid, 1 = valid.
|
||||||
|
subdomain: string; // Subdomain of the instance.
|
||||||
|
uid: number; // User id, same as in user_fields_data.
|
||||||
|
validation_code: string; // Validation code for the instance.
|
||||||
|
}
|
||||||
102
src/provisioner.ts
Normal file
102
src/provisioner.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import Account, { initAccount } from "./models/account";
|
||||||
|
import User, { initUser } from "./models/user";
|
||||||
|
import { provisionInstance } from "./websocketHandler";
|
||||||
|
import { errorLogger, websocketLogger } from "./logging/log";
|
||||||
|
import { sequelize } from "./db/connection";
|
||||||
|
import WisskiInstanceModel from "./models/wisskiInstance";
|
||||||
|
import mailer from "./mailer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the event that is emitted when a provision request is queued.
|
||||||
|
*/
|
||||||
|
const PROVISION_EVENT = 'provision';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that handles provisioning of instances.
|
||||||
|
*/
|
||||||
|
export default class Provisioner {
|
||||||
|
private emitter: EventEmitter;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.emitter.on(PROVISION_EVENT, async (wisskiInstance: WisskiInstanceModel) => {
|
||||||
|
// Define your User model
|
||||||
|
const User = initUser(sequelize);
|
||||||
|
|
||||||
|
// Define your WisskiCloudAccountManagerAccounts model
|
||||||
|
const Account = initAccount(sequelize);
|
||||||
|
|
||||||
|
// Define associations
|
||||||
|
User.hasOne(Account, { foreignKey: 'uid' });
|
||||||
|
Account.belongsTo(User, { foreignKey: 'uid' });
|
||||||
|
const account = await Account.findOne({
|
||||||
|
include: {
|
||||||
|
model: User,
|
||||||
|
where: { uid: wisskiInstance.uid }
|
||||||
|
}
|
||||||
|
}) as Account & { User: User };
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
errorLogger.error('account not found for account id:', wisskiInstance.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
account.set('provisioned', 1);
|
||||||
|
await account.save()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.doProvision(wisskiInstance);
|
||||||
|
const credentials = {
|
||||||
|
domain: result.url,
|
||||||
|
user: result.username,
|
||||||
|
password: result.password,
|
||||||
|
name: account.User.dataValues.name,
|
||||||
|
status: result.success ? 'success' : 'error',
|
||||||
|
message: result.message
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
websocketLogger.info('provision succeeded for subdomain:', wisskiInstance.subdomain);
|
||||||
|
mailer(account.User.dataValues.mail, credentials);
|
||||||
|
account.set('provisioned', 2);
|
||||||
|
await account.save();
|
||||||
|
} else {
|
||||||
|
websocketLogger.error('provision failed for subdomain:', wisskiInstance.subdomain, 'error: ', result.message);
|
||||||
|
mailer(account.User.dataValues.mail, credentials);
|
||||||
|
account.set('provisioned', 3);
|
||||||
|
await account.save();
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
websocketLogger.error('provision failed for subdomain:', wisskiInstance.subdomain);
|
||||||
|
errorLogger.error('provision failed for subdomain:', wisskiInstance.subdomain, 'error: ', error);
|
||||||
|
account.set('provisioned', 3);
|
||||||
|
await account.save();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do the actual provision.
|
||||||
|
*
|
||||||
|
* @param WisskiInstanceModel wisskiInstance
|
||||||
|
* The subdomain of the instance to provision.
|
||||||
|
* @returns A promise that resolves to an object with the properties
|
||||||
|
* success, message, username, and password.
|
||||||
|
*/
|
||||||
|
async doProvision(wisskiInstance: WisskiInstanceModel) {
|
||||||
|
websocketLogger.info("starting provision for subdomain:", wisskiInstance.subdomain)
|
||||||
|
const status = await provisionInstance(wisskiInstance.subdomain);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WisskiInstanceModel} wisskiInstance
|
||||||
|
*/
|
||||||
|
queue(wisskiInstance: WisskiInstanceModel) {
|
||||||
|
websocketLogger.info(`queueing subdomain: ${wisskiInstance.subdomain} for account: ${wisskiInstance.name}`);
|
||||||
|
this.emitter.emit(PROVISION_EVENT, wisskiInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BIN
src/public/favicon.ico
Normal file
BIN
src/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
28
src/templates/credentialMail.ejs
Normal file
28
src/templates/credentialMail.ejs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<table style="width:100%; border:1px solid black; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" style="text-align:center; background-color: #f2f2f2;">
|
||||||
|
<b>Hello <%= credentials.name %>!</b>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="text-align:center;">
|
||||||
|
Your WissKI Cloud Instance <a href="<%= credentials.domain %>"
|
||||||
|
target="_blank"><%= credentials.domain %></a> is ready!
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:right; background-color: #f2f2f2;">
|
||||||
|
<strong>User:</strong></td>
|
||||||
|
<td style="text-align:left;"><%= credentials.user %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:right; background-color: #f2f2f2;">
|
||||||
|
<strong>Password:</strong></td>
|
||||||
|
<td style="text-align:left;"><%= credentials.password %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="text-align:center;">
|
||||||
|
Please alter your Password for security reasons. Have fun!
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
7
src/templates/errorMail.ejs
Normal file
7
src/templates/errorMail.ejs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div style="width:100%; border:1px solid black; border-collapse: collapse;">
|
||||||
|
<p>
|
||||||
|
<div><b>Hello <%= credentials.name %>!</b></div>
|
||||||
|
<div>Something went wrong with your WissKI Cloud Instance <%= credentials.domain %>, it could not provisioned! <br /></div>
|
||||||
|
<div>Message: <%= credentials.message %></div>
|
||||||
|
<div>Please login in your WissKI Cloud Account, delete the instance and try again. If the problem remains, please contact <a href="mailto:cloud@wiss-ki.eu">our staff.</a></div>
|
||||||
|
</p>
|
||||||
23
src/test/healthcheck.spec.ts
Normal file
23
src/test/healthcheck.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Done } from "mocha";
|
||||||
|
import request from "supertest";
|
||||||
|
import app from "../app";
|
||||||
|
|
||||||
|
// Test suite
|
||||||
|
describe('Testing to check health', function() {
|
||||||
|
// Test case
|
||||||
|
it('Should handle a request to check health', function(done: Done) {
|
||||||
|
request(app)
|
||||||
|
.get('/api/v1/healthcheck')
|
||||||
|
.expect(200)
|
||||||
|
.end((error: Error | null, response: request.Response) => {
|
||||||
|
if (error) {
|
||||||
|
done(error);
|
||||||
|
} else {
|
||||||
|
const res = response.text;
|
||||||
|
res.should.not.equal(null, 'response should contain a text message');
|
||||||
|
res.should.equal('API is up and running', 'Should return working message');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
83
src/websocket/calls.ts
Normal file
83
src/websocket/calls.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { WebSocketCall } from ".";
|
||||||
|
|
||||||
|
/** Backup backups everything */
|
||||||
|
export function Backup(): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'backup',
|
||||||
|
'params': [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisionParams = {
|
||||||
|
Slug: string;
|
||||||
|
Flavor?: "Drupal 10" | "Drupal 9",
|
||||||
|
System: SystemParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemParams = {
|
||||||
|
PHP: "Default (8.1)" | "8.0" | "8.1" | "8.2",
|
||||||
|
OpCacheDevelopment: boolean,
|
||||||
|
ContentSecurityPolicy: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provision provisions a new instance */
|
||||||
|
export function Provision(params: ProvisionParams): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'provision',
|
||||||
|
'params': [
|
||||||
|
JSON.stringify(params)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshot makes a snapshot of an instance */
|
||||||
|
export function Snapshot(subdomain: string): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'snapshot',
|
||||||
|
'params': [subdomain],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebuild rebuilds an instance */
|
||||||
|
export function Rebuild(subdomain: string, params: SystemParams) {
|
||||||
|
return {
|
||||||
|
'call': 'rebuild',
|
||||||
|
'params': [
|
||||||
|
subdomain,
|
||||||
|
JSON.stringify(params)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update updates a specific instance */
|
||||||
|
export function Update(subdomain: string): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'update',
|
||||||
|
'params': [subdomain],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Start starts a specific instance */
|
||||||
|
export function Start(subdomain: string): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'start',
|
||||||
|
'params': [subdomain],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop stops a specific instance */
|
||||||
|
export function Stop(subdomain: string): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'stop',
|
||||||
|
'params': [subdomain],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Purge purges a specific instance */
|
||||||
|
export function Purge(subdomain: string): WebSocketCall {
|
||||||
|
return {
|
||||||
|
'call': 'purge',
|
||||||
|
'params': [subdomain],
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/websocket/index.ts
Normal file
105
src/websocket/index.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
/** @file implements the websocket protocol used by the distillery */
|
||||||
|
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import { errorLogger} from "../logging/log";
|
||||||
|
|
||||||
|
/** A call to the websocket endpoint */
|
||||||
|
export interface WebSocketCall {
|
||||||
|
call: string;
|
||||||
|
params: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the result of a websocket call */
|
||||||
|
export interface WebSocketResult {
|
||||||
|
message: string,
|
||||||
|
password: string,
|
||||||
|
success: boolean,
|
||||||
|
username: string,
|
||||||
|
url: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** optional hooks to call when something happens */
|
||||||
|
export interface Hooks {
|
||||||
|
beforeCall: (call: WebSocketCall) => void; // called right before sending the request
|
||||||
|
afterCall: (call: WebSocketCall, result: WebSocketResult) => void; // called when the socket is closed
|
||||||
|
onError: (call: WebSocketCall, error: any) => void; // called when an error occurs before rejecting the promise
|
||||||
|
onLogLine: (call: WebSocketCall, line: string) => void; // called when a log line is received
|
||||||
|
}
|
||||||
|
|
||||||
|
/** specifies a remote endpoint */
|
||||||
|
export interface Remote {
|
||||||
|
url: string; // the remote websocket url to talk to
|
||||||
|
token?: string; // optional token
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a websocket remote call
|
||||||
|
*
|
||||||
|
* @param remote the remote to call
|
||||||
|
* @param call the call to make
|
||||||
|
* @param hooks optional hooks to call when something happens
|
||||||
|
*
|
||||||
|
* @returns a promise that resolves to the result of the call
|
||||||
|
*/
|
||||||
|
export default async function Call(remote: Remote, call: WebSocketCall, hooks?: Partial<Hooks>): Promise<WebSocketResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let options = { headers: {} };
|
||||||
|
if (remote.token) {
|
||||||
|
options.headers = { 'Authorization': 'Bearer ' + remote.token };
|
||||||
|
}
|
||||||
|
const ws = new WebSocket(remote.url, options);
|
||||||
|
|
||||||
|
let result = {'success': false, 'message': 'Unknown error'};
|
||||||
|
let username = '';
|
||||||
|
let password = '';
|
||||||
|
let url = '';
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
if (hooks && hooks.onError) {
|
||||||
|
hooks.onError(call, err);
|
||||||
|
}
|
||||||
|
reject(err)
|
||||||
|
});
|
||||||
|
ws.on('open', () => {
|
||||||
|
if (hooks && hooks.beforeCall) {
|
||||||
|
hooks.beforeCall(call);
|
||||||
|
}
|
||||||
|
ws.send(Buffer.from(JSON.stringify(call), 'utf8'));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', async (msg, isBinary) => {
|
||||||
|
if (!isBinary) {
|
||||||
|
if (hooks && hooks.onLogLine) {
|
||||||
|
hooks.onLogLine(call, msg.toString());
|
||||||
|
try {
|
||||||
|
if (msg.toString().includes('URL')) {
|
||||||
|
url = msg.toString().split(': ')[1].trim();
|
||||||
|
}
|
||||||
|
if (msg.toString().includes('Username')) {
|
||||||
|
username = msg.toString().split(': ')[1].trim();
|
||||||
|
}
|
||||||
|
if (msg.toString().includes('Password')) {
|
||||||
|
password = msg.toString().split(': ')[1].trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorLogger.error('error parsing log line', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result = JSON.parse(msg.toString());
|
||||||
|
});
|
||||||
|
// @todo add username and pass only if provided.
|
||||||
|
ws.on('close', () => {
|
||||||
|
if (hooks && hooks.afterCall) {
|
||||||
|
hooks.afterCall(call, {...result, password, url, username});
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
...result,
|
||||||
|
password,
|
||||||
|
url,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
72
src/websocketHandler.ts
Normal file
72
src/websocketHandler.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* This module exports functions to handle websocket communication with the server.
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {appLogger, errorLogger, websocketLogger} from "./logging/log";
|
||||||
|
import {appConfig} from "./config/appConfig";
|
||||||
|
import Call, { Remote, Hooks, WebSocketResult } from './websocket/index';
|
||||||
|
import * as calls from './websocket/calls';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default remote for use within this app.
|
||||||
|
* @returns The default remote object containing the websocket URL and token.
|
||||||
|
*/
|
||||||
|
export function defaultRemote(): Remote {
|
||||||
|
return {
|
||||||
|
'url': appConfig.websocketUrl,
|
||||||
|
'token': appConfig.websocketToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default hooks for use within this app.
|
||||||
|
* @returns The default hooks object containing beforeCall, afterCall, onError, and onLogLine functions.
|
||||||
|
*/
|
||||||
|
export function defaultHooks(): Partial<Hooks> {
|
||||||
|
return {
|
||||||
|
beforeCall: (call) => {
|
||||||
|
websocketLogger.info('sending websocket call', call);
|
||||||
|
},
|
||||||
|
afterCall: (call, result) => {
|
||||||
|
websocketLogger.info('call', call, 'got result', result.success);
|
||||||
|
},
|
||||||
|
onError: (call, err) => {
|
||||||
|
errorLogger.info('call', call, 'got error', err);
|
||||||
|
},
|
||||||
|
onLogLine: (call, msg) => {
|
||||||
|
appLogger.debug(msg.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provise an instance.
|
||||||
|
*
|
||||||
|
* @param subdomain - The subdomain of the instance to provision.
|
||||||
|
* @returns Promise <{success: boolean, message: string, user: string, pass:string}>
|
||||||
|
* A promise that resolves to an object with the properties success and message.
|
||||||
|
*/
|
||||||
|
async function provisionInstance(subdomain: string): Promise<WebSocketResult> {
|
||||||
|
return Call(
|
||||||
|
defaultRemote(),
|
||||||
|
calls.Provision({Slug: subdomain, Flavor: "Drupal 10", System: { "PHP": "Default (8.1)", "OpCacheDevelopment": false, "ContentSecurityPolicy": "" }}),
|
||||||
|
defaultHooks(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge an instance.
|
||||||
|
*
|
||||||
|
* @param subdomain - The subdomain of the instance to purge.
|
||||||
|
* @returns A promise that resolves to an object with the properties success and message.
|
||||||
|
*/
|
||||||
|
async function purgeInstance(subdomain: string): Promise<{success: boolean, message: string}> {
|
||||||
|
return Call(
|
||||||
|
defaultRemote(),
|
||||||
|
calls.Purge(subdomain),
|
||||||
|
defaultHooks(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {provisionInstance, purgeInstance};
|
||||||
109
tsconfig.json
Normal file
109
tsconfig.json
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "esnext", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
|
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./build", /* Specify an output folder for all emitted files. */
|
||||||
|
"removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
"noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
51
webpack.config.js
Normal file
51
webpack.config.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
const path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const isDevelopment = argv.mode === 'development';
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry: './src/bin/daemon.ts',
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
target: 'node',
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ejs', '.tsx', '.ts', '.js'],
|
||||||
|
modules: [
|
||||||
|
path.resolve(__dirname, 'src'),
|
||||||
|
'node_modules'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
devtool: isDevelopment ? 'inline-source-map' : 'source-map',
|
||||||
|
watch: isDevelopment,
|
||||||
|
watchOptions: {
|
||||||
|
ignored: [
|
||||||
|
'node_modules/**',
|
||||||
|
'logs/**',
|
||||||
|
],
|
||||||
|
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: 'src/templates', to: 'templates' },
|
||||||
|
{ from: 'src/public', to: 'public' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// Add any other configuration options specific to development or production here
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue