first commit

This commit is contained in:
wisski 2024-04-30 14:51:28 +02:00
commit dc354cf586
37 changed files with 8225 additions and 0 deletions

9
.editorconfig Normal file
View 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
View 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
View file

@ -0,0 +1 @@
webpack.config.js

6
.eslintrc.cjs Normal file
View 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
View 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
View file

@ -0,0 +1,3 @@
{
"esversion": 11
}

10
.travis.yml Normal file
View file

@ -0,0 +1,10 @@
{
"language": "node_js",
"node_js": "10",
"services": [
"mongodb"
],
"script": [
"npm run test"
]
}

247
api-spec.yaml Normal file
View 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

File diff suppressed because it is too large Load diff

73
package.json Normal file
View 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
View 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;

View 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};

View 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;

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
};
};