Merge branch 'main' into new-admin
This commit is contained in:
commit
4602aaa871
11
.eslintrc.cjs
Normal file
11
.eslintrc.cjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript/recommended",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
};
|
|
@ -22,10 +22,11 @@
|
|||
"test": "yarn run server:test",
|
||||
"build": "yarn run server:build && grunt build && rsync -avzL --exclude='*/__mocks__/' --exclude='*.test.*' server-build/ dist/server/",
|
||||
"clean": "rm -rf server-build/ dist/ && grunt clean",
|
||||
"dist": "yarn run clean && yarn run build && ./bin/dist-fix-symlinks.sh && yarn run test",
|
||||
"dist": "yarn run clean && yarn run build && yarn run server:lint && ./bin/dist-fix-symlinks.sh && yarn run test",
|
||||
"client:serve": "grunt serve",
|
||||
"server:test": "jest --config=jest.server.config.js",
|
||||
"server:build": "tsc -b server && ln -sfv ../../server/db/patches ./server-build/db/ && ln -sfv ../server/templates ./server-build/ && ln -sfv ../server/mailTemplates ./server-build/",
|
||||
"server:lint": "eslint server/ --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"server:run": "yarn run server:build && node server-build/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -56,6 +57,7 @@
|
|||
"sqlite3": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/async": "^3.2.15",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/command-line-args": "^5.2.0",
|
||||
|
@ -72,8 +74,11 @@
|
|||
"@types/node-cron": "^3.0.2",
|
||||
"@types/nodemailer": "^6.4.5",
|
||||
"@types/request": "^2.48.8",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"bower": "^1.8.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"eslint": "^8.22.0",
|
||||
"grunt": "^1.4.1",
|
||||
"grunt-cli": "^1.4.3",
|
||||
"grunt-concurrent": "^3.0.0",
|
||||
|
@ -98,6 +103,7 @@
|
|||
"jest": "^28.1.3",
|
||||
"jshint-stylish": "^2.2.1",
|
||||
"load-grunt-tasks": "^5.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"time-grunt": "^2.0.0",
|
||||
"ts-jest": "^28.0.8",
|
||||
"typescript": "^4.7.4",
|
||||
|
|
6
server/@types/http-auth-connect/index.d.ts
vendored
6
server/@types/http-auth-connect/index.d.ts
vendored
|
@ -1,6 +1,6 @@
|
|||
declare module "http-auth-connect" {
|
||||
import {Auth} from "http-auth";
|
||||
import {RequestHandler} from "express"
|
||||
import { Auth } from "http-auth";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export default function (auth: Auth): RequestHandler
|
||||
export default function (auth: Auth): RequestHandler;
|
||||
}
|
||||
|
|
17
server/@types/http-auth/index.d.ts
vendored
17
server/@types/http-auth/index.d.ts
vendored
|
@ -4,9 +4,18 @@ declare module "http-auth" {
|
|||
class BasicAuth extends Auth {}
|
||||
class BasicAuthOptions {}
|
||||
|
||||
type BasicAuthChecker =
|
||||
(username: string, password: string, callback: BasicAuthCheckerCallback) => void
|
||||
type BasicAuthCheckerCallback = (result: boolean | Error, customUser?: string) => void
|
||||
type BasicAuthChecker = (
|
||||
username: string,
|
||||
password: string,
|
||||
callback: BasicAuthCheckerCallback
|
||||
) => void;
|
||||
type BasicAuthCheckerCallback = (
|
||||
result: boolean | Error,
|
||||
customUser?: string
|
||||
) => void;
|
||||
|
||||
function basic(options: BasicAuthOptions, checker: BasicAuthChecker): BasicAuth
|
||||
function basic(
|
||||
options: BasicAuthOptions,
|
||||
checker: BasicAuthChecker
|
||||
): BasicAuth;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
declare module "nodemailer-html-to-text" {
|
||||
import {PluginFunction} from "nodemailer/lib/mailer";
|
||||
import {HtmlToTextOptions} from "html-to-text";
|
||||
import { PluginFunction } from "nodemailer/lib/mailer";
|
||||
import { HtmlToTextOptions } from "html-to-text";
|
||||
|
||||
export function htmlToText(options: HtmlToTextOptions): PluginFunction;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {MockLogger} from "./logger";
|
||||
import { MockLogger } from "./logger";
|
||||
|
||||
test("should reset single message", () => {
|
||||
// given
|
||||
|
@ -152,7 +152,10 @@ test("should get multiple messages", () => {
|
|||
logger.tag("foo", "bar").debug("message 2");
|
||||
|
||||
// then
|
||||
expect(logger.getMessages("debug", "foo", "bar")).toEqual([["message 1"], ["message 2"]]);
|
||||
expect(logger.getMessages("debug", "foo", "bar")).toEqual([
|
||||
["message 1"],
|
||||
["message 2"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("should get complex message", () => {
|
||||
|
@ -163,5 +166,7 @@ test("should get complex message", () => {
|
|||
logger.tag("foo", "bar").debug("message", 1, false, {});
|
||||
|
||||
// then
|
||||
expect(logger.getMessages("debug", "foo", "bar")).toEqual([["message", 1, false, {}]]);
|
||||
expect(logger.getMessages("debug", "foo", "bar")).toEqual([
|
||||
["message", 1, false, {}],
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
import {Logger, TaggedLogger, LogLevel} from '../types';
|
||||
import {ActivatableLogger} from '../logger';
|
||||
import { LogLevel, TaggedLogger } from "../types";
|
||||
import { ActivatableLogger } from "../logger";
|
||||
|
||||
export type MockLogMessages = any[][];
|
||||
export type MockLogMessages = unknown[][];
|
||||
type TaggedLogMessages = {
|
||||
tags: {[key: string]: TaggedLogMessages},
|
||||
logs: {[key: string]: MockLogMessages}
|
||||
}
|
||||
tags: { [key: string]: TaggedLogMessages };
|
||||
logs: { [key: string]: MockLogMessages };
|
||||
};
|
||||
|
||||
export class MockLogger implements ActivatableLogger {
|
||||
private taggedLogMessages: TaggedLogMessages = MockLogger.emptyTaggedLogMessages();
|
||||
private taggedLogMessages: TaggedLogMessages =
|
||||
MockLogger.emptyTaggedLogMessages();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor() {}
|
||||
|
||||
private static emptyTaggedLogMessages(): TaggedLogMessages {
|
||||
return {
|
||||
tags: {},
|
||||
logs: {}
|
||||
logs: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,46 +38,54 @@ export class MockLogger implements ActivatableLogger {
|
|||
return taggedLogMessages.logs[level] || [];
|
||||
}
|
||||
|
||||
init(...args: any[]): void {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
|
||||
init(...args: unknown[]): void {}
|
||||
|
||||
private doLog(taggedLogMessages: TaggedLogMessages, level: LogLevel, tags: string[], args: any[]): void {
|
||||
private doLog(
|
||||
taggedLogMessages: TaggedLogMessages,
|
||||
level: LogLevel,
|
||||
tags: string[],
|
||||
args: unknown[]
|
||||
): void {
|
||||
if (tags.length > 0) {
|
||||
const tag = tags[0];
|
||||
const remainingTags = tags.slice(1);
|
||||
const subTaggedLogsMessages: TaggedLogMessages =
|
||||
taggedLogMessages.tags[tag] || MockLogger.emptyTaggedLogMessages();
|
||||
taggedLogMessages.tags[tag] ||
|
||||
MockLogger.emptyTaggedLogMessages();
|
||||
this.doLog(subTaggedLogsMessages, level, remainingTags, args);
|
||||
taggedLogMessages.tags[tag] = subTaggedLogsMessages;
|
||||
|
||||
} else {
|
||||
const logMessages: MockLogMessages = taggedLogMessages.logs[level] || [];
|
||||
const logMessages: MockLogMessages =
|
||||
taggedLogMessages.logs[level] || [];
|
||||
logMessages.push(args);
|
||||
taggedLogMessages.logs[level] = logMessages;
|
||||
}
|
||||
}
|
||||
|
||||
tag(...tags: string[]): TaggedLogger {
|
||||
const logger: MockLogger = this;
|
||||
const doLog = this.doLog.bind(this);
|
||||
const taggedLogMessages = this.taggedLogMessages;
|
||||
return {
|
||||
log(level: LogLevel, ...args: any[]): void {
|
||||
logger.doLog(logger.taggedLogMessages, level, tags, args);
|
||||
log(level: LogLevel, ...args: unknown[]): void {
|
||||
doLog(taggedLogMessages, level, tags, args);
|
||||
},
|
||||
debug(...args: any[]): void {
|
||||
this.log('debug', ...args);
|
||||
debug(...args: unknown[]): void {
|
||||
this.log("debug", ...args);
|
||||
},
|
||||
info(...args: any[]): void {
|
||||
this.log('info', ...args);
|
||||
info(...args: unknown[]): void {
|
||||
this.log("info", ...args);
|
||||
},
|
||||
warn(...args: any[]): void {
|
||||
this.log('warn', ...args);
|
||||
warn(...args: unknown[]): void {
|
||||
this.log("warn", ...args);
|
||||
},
|
||||
error(...args: any[]): void {
|
||||
this.log('error', ...args);
|
||||
error(...args: unknown[]): void {
|
||||
this.log("error", ...args);
|
||||
},
|
||||
profile(...args: any[]): void {
|
||||
this.log('profile', ...args);
|
||||
profile(...args: unknown[]): void {
|
||||
this.log("profile", ...args);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import _ from "lodash";
|
||||
import auth, {BasicAuthCheckerCallback} from "http-auth";
|
||||
import auth, { BasicAuthCheckerCallback } from "http-auth";
|
||||
import authConnect from "http-auth-connect";
|
||||
import bodyParser from "body-parser";
|
||||
import bcrypt from "bcrypt";
|
||||
import compress from "compression";
|
||||
import express, {Express, NextFunction, Request, Response} from "express";
|
||||
import {promises as fs} from "graceful-fs";
|
||||
import express, { Express, NextFunction, Request, Response } from "express";
|
||||
import { promises as fs } from "graceful-fs";
|
||||
|
||||
import {config} from "./config";
|
||||
import type {CleartextPassword, PasswordHash, Username} from "./types";
|
||||
import {isString} from "./types";
|
||||
import { config } from "./config";
|
||||
import type { CleartextPassword, PasswordHash, Username } from "./types";
|
||||
import { isString } from "./types";
|
||||
import Logger from "./logger";
|
||||
|
||||
export const app: Express = express();
|
||||
|
@ -17,7 +17,8 @@ export const app: Express = express();
|
|||
/**
|
||||
* Used to have some password comparison in case the user does not exist to avoid timing attacks.
|
||||
*/
|
||||
const INVALID_PASSWORD_HASH: PasswordHash = "$2b$05$JebmV1q/ySuxa89GoJYlc.6SEnj1OZYBOfTf.TYAehcC5HLeJiWPi" as PasswordHash;
|
||||
const INVALID_PASSWORD_HASH: PasswordHash =
|
||||
"$2b$05$JebmV1q/ySuxa89GoJYlc.6SEnj1OZYBOfTf.TYAehcC5HLeJiWPi" as PasswordHash;
|
||||
|
||||
/**
|
||||
* Trying to implement a timing safe string compare.
|
||||
|
@ -41,7 +42,10 @@ function timingSafeEqual<T extends string>(a: T, b: T): boolean {
|
|||
return different === 0;
|
||||
}
|
||||
|
||||
async function isValidLogin(username: Username, password: CleartextPassword): Promise<boolean> {
|
||||
async function isValidLogin(
|
||||
username: Username,
|
||||
password: CleartextPassword
|
||||
): Promise<boolean> {
|
||||
if (!config.server.internal.active) {
|
||||
return false;
|
||||
}
|
||||
|
@ -71,52 +75,63 @@ export function init(): void {
|
|||
// urls beneath /internal are protected
|
||||
const internalAuth = auth.basic(
|
||||
{
|
||||
realm: 'Knotenformular - Intern'
|
||||
realm: "Knotenformular - Intern",
|
||||
},
|
||||
function (username: string, password: string, callback: BasicAuthCheckerCallback): void {
|
||||
function (
|
||||
username: string,
|
||||
password: string,
|
||||
callback: BasicAuthCheckerCallback
|
||||
): void {
|
||||
isValidLogin(username as Username, password as CleartextPassword)
|
||||
.then(result => callback(result))
|
||||
.catch(err => {
|
||||
Logger.tag('login').error(err);
|
||||
.then((result) => callback(result))
|
||||
.catch((err) => {
|
||||
Logger.tag("login").error(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
router.use('/internal', authConnect(internalAuth));
|
||||
router.use("/internal", authConnect(internalAuth));
|
||||
|
||||
router.use(bodyParser.json());
|
||||
router.use(bodyParser.urlencoded({extended: true}));
|
||||
router.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
const adminDir = __dirname + '/../admin';
|
||||
const clientDir = __dirname + '/../client';
|
||||
const templateDir = __dirname + '/templates';
|
||||
const adminDir = __dirname + "/../admin";
|
||||
const clientDir = __dirname + "/../client";
|
||||
const templateDir = __dirname + "/templates";
|
||||
|
||||
const jsTemplateFiles = [
|
||||
'/config.js'
|
||||
];
|
||||
const jsTemplateFiles = ["/config.js"];
|
||||
|
||||
function usePromise(f: (req: Request, res: Response) => Promise<void>): void {
|
||||
function usePromise(
|
||||
f: (req: Request, res: Response) => Promise<void>
|
||||
): void {
|
||||
router.use((req: Request, res: Response, next: NextFunction): void => {
|
||||
f(req, res).then(next).catch(next)
|
||||
f(req, res).then(next).catch(next);
|
||||
});
|
||||
}
|
||||
|
||||
router.use(compress());
|
||||
|
||||
async function serveTemplate(mimeType: string, req: Request, res: Response): Promise<void> {
|
||||
const body = await fs.readFile(templateDir + '/' + req.path + '.template', 'utf8');
|
||||
async function serveTemplate(
|
||||
mimeType: string,
|
||||
req: Request,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
const body = await fs.readFile(
|
||||
templateDir + "/" + req.path + ".template",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
res.writeHead(200, {'Content-Type': mimeType});
|
||||
res.end(_.template(body)({config: config.client}));
|
||||
res.writeHead(200, { "Content-Type": mimeType });
|
||||
res.end(_.template(body)({ config: config.client }));
|
||||
}
|
||||
|
||||
usePromise(async (req: Request, res: Response): Promise<void> => {
|
||||
if (jsTemplateFiles.indexOf(req.path) >= 0) {
|
||||
await serveTemplate('application/javascript', req, res);
|
||||
await serveTemplate("application/javascript", req, res);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/internal/admin', express.static(adminDir + '/'));
|
||||
router.use('/', express.static(clientDir + '/'));
|
||||
router.use("/internal/admin", express.static(adminDir + "/"));
|
||||
router.use("/", express.static(clientDir + "/"));
|
||||
|
||||
app.use(config.server.rootPath, router);
|
||||
}
|
||||
|
|
|
@ -1,46 +1,65 @@
|
|||
import commandLineArgs from "command-line-args"
|
||||
import commandLineUsage from "command-line-usage"
|
||||
import fs from "graceful-fs"
|
||||
import url from "url"
|
||||
import {parse} from "sparkson"
|
||||
import {Config, Url, Version} from "./types"
|
||||
import commandLineArgs from "command-line-args";
|
||||
import commandLineUsage from "command-line-usage";
|
||||
import fs from "graceful-fs";
|
||||
import url from "url";
|
||||
import { parse } from "sparkson";
|
||||
import { Config, hasOwnProperty, Url, Version } from "./types";
|
||||
|
||||
// @ts-ignore
|
||||
export let config: Config = {};
|
||||
export let config: Config = {} as Config;
|
||||
export let version: Version = "unknown" as Version;
|
||||
|
||||
export function parseCommandLine(): void {
|
||||
const commandLineDefs = [
|
||||
{name: 'help', alias: 'h', type: Boolean, description: 'Show this help'},
|
||||
{name: 'config', alias: 'c', type: String, description: 'Location of config.json'},
|
||||
{name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version'}
|
||||
{
|
||||
name: "help",
|
||||
alias: "h",
|
||||
type: Boolean,
|
||||
description: "Show this help",
|
||||
},
|
||||
{
|
||||
name: "config",
|
||||
alias: "c",
|
||||
type: String,
|
||||
description: "Location of config.json",
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
alias: "v",
|
||||
type: Boolean,
|
||||
description: "Show ffffng version",
|
||||
},
|
||||
];
|
||||
|
||||
let commandLineOptions;
|
||||
try {
|
||||
commandLineOptions = commandLineArgs(commandLineDefs);
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
console.error(e.message);
|
||||
} catch (error) {
|
||||
if (hasOwnProperty(error, "message")) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error(e);
|
||||
console.error(error);
|
||||
}
|
||||
console.error('Try \'--help\' for more information.');
|
||||
console.error("Try '--help' for more information.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonFile = __dirname + '/../package.json';
|
||||
const packageJsonFile = __dirname + "/../package.json";
|
||||
if (fs.existsSync(packageJsonFile)) {
|
||||
version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version;
|
||||
version = JSON.parse(fs.readFileSync(packageJsonFile, "utf8")).version;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log(commandLineUsage([
|
||||
{
|
||||
header: 'ffffng - ' + version + ' - Freifunk node management form',
|
||||
optionList: commandLineDefs
|
||||
}
|
||||
]));
|
||||
console.log(
|
||||
commandLineUsage([
|
||||
{
|
||||
header:
|
||||
"ffffng - " +
|
||||
version +
|
||||
" - Freifunk node management form",
|
||||
optionList: commandLineDefs,
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (commandLineOptions.help) {
|
||||
|
@ -49,7 +68,7 @@ export function parseCommandLine(): void {
|
|||
}
|
||||
|
||||
if (commandLineOptions.version) {
|
||||
console.log('ffffng - ' + version);
|
||||
console.log("ffffng - " + version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
@ -62,9 +81,9 @@ export function parseCommandLine(): void {
|
|||
let configJSON = {};
|
||||
|
||||
if (fs.existsSync(configJSONFile)) {
|
||||
configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8'));
|
||||
configJSON = JSON.parse(fs.readFileSync(configJSONFile, "utf8"));
|
||||
} else {
|
||||
console.error('config.json not found: ' + configJSONFile);
|
||||
console.error("config.json not found: " + configJSONFile);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -72,7 +91,7 @@ export function parseCommandLine(): void {
|
|||
|
||||
function stripTrailingSlash(url: Url): Url {
|
||||
return url.endsWith("/")
|
||||
? url.substring(0, url.length - 1) as Url
|
||||
? (url.substring(0, url.length - 1) as Url)
|
||||
: url;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,48 +1,83 @@
|
|||
import {RunResult, SqlType, Statement, TypedDatabase} from "../../types";
|
||||
import { RunResult, SqlType, Statement, TypedDatabase } from "../../types";
|
||||
import * as sqlite3 from "sqlite3";
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
export class MockDatabase implements TypedDatabase {
|
||||
constructor() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor() {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async on(event: string, listener: unknown): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async on(event: string, listener: any): Promise<void> {
|
||||
}
|
||||
|
||||
async run(sql: SqlType, ...params: any[]): Promise<RunResult> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async run(sql: SqlType, ...params: unknown[]): Promise<RunResult> {
|
||||
return {
|
||||
stmt: new Statement(new sqlite3.Statement()),
|
||||
};
|
||||
}
|
||||
|
||||
async get<T = any>(sql: SqlType, ...params: any[]): Promise<T | undefined> {
|
||||
async get<T = unknown>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
sql: SqlType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
...params: unknown[]
|
||||
): Promise<T | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async each<T = any>(sql: SqlType, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T = any>(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T = any>(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T = any>(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T = any>(sql: SqlType, ...params: any[]): Promise<number>;
|
||||
async each(sql: SqlType, ...callback: (any)[]): Promise<number> {
|
||||
async each<T = unknown>(
|
||||
sql: SqlType,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T = unknown>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T = unknown>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T = unknown>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
param3: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async each<T = unknown>(
|
||||
sql: SqlType,
|
||||
...params: unknown[]
|
||||
): Promise<number>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async each(sql: SqlType, ...callback: unknown[]): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
async all<T>(sql: SqlType, ...params: any[]): Promise<T[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async all<T>(sql: SqlType, ...params: unknown[]): Promise<T[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async exec(sql: SqlType, ...params: any[]): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async exec(sql: SqlType, ...params: unknown[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
async prepare(sql: SqlType, ...params: any[]): Promise<Statement> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async prepare(sql: SqlType, ...params: unknown[]): Promise<Statement> {
|
||||
return new Statement(new sqlite3.Statement());
|
||||
}
|
||||
}
|
||||
|
||||
export const db: MockDatabase = new MockDatabase();
|
||||
|
||||
export {TypedDatabase, Statement}
|
||||
export { TypedDatabase, Statement };
|
||||
|
|
|
@ -2,11 +2,11 @@ import util from "util";
|
|||
import fs from "graceful-fs";
|
||||
import glob from "glob";
|
||||
import path from "path";
|
||||
import {config} from "../config";
|
||||
import { config } from "../config";
|
||||
import Logger from "../logger";
|
||||
import {Database, open, Statement} from "sqlite";
|
||||
import { Database, open, Statement } from "sqlite";
|
||||
import * as sqlite3 from "sqlite3";
|
||||
import {RunResult, SqlType, TypedDatabase} from "../types";
|
||||
import { RunResult, SqlType, TypedDatabase } from "../types";
|
||||
|
||||
const pglob = util.promisify(glob);
|
||||
const pReadFile = util.promisify(fs.readFile);
|
||||
|
@ -28,102 +28,145 @@ class DatabasePromiseWrapper implements TypedDatabase {
|
|||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
this.db.catch(err => {
|
||||
Logger.tag('database', 'init').error('Error initializing database: ', err);
|
||||
this.db.catch((err) => {
|
||||
Logger.tag("database", "init").error(
|
||||
"Error initializing database: ",
|
||||
err
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
async on(event: string, listener: any): Promise<void> {
|
||||
async on(event: string, listener: unknown): Promise<void> {
|
||||
const db = await this.db;
|
||||
db.on(event, listener);
|
||||
}
|
||||
|
||||
async run(sql: SqlType, ...params: any[]): Promise<RunResult> {
|
||||
async run(sql: SqlType, ...params: unknown[]): Promise<RunResult> {
|
||||
const db = await this.db;
|
||||
return db.run(sql, ...params);
|
||||
}
|
||||
|
||||
async get<T>(sql: SqlType, ...params: any[]): Promise<T | undefined> {
|
||||
async get<T>(sql: SqlType, ...params: unknown[]): Promise<T | undefined> {
|
||||
const db = await this.db;
|
||||
return await db.get<T>(sql, ...params);
|
||||
}
|
||||
|
||||
async each<T>(sql: SqlType, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T>(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T>(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T>(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
async each<T>(sql: SqlType, ...params: any[]): Promise<number> {
|
||||
async each<T>(
|
||||
sql: SqlType,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
async each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
param3: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async each<T>(sql: SqlType, ...params: unknown[]): Promise<number> {
|
||||
const db = await this.db;
|
||||
// @ts-ignore
|
||||
return await db.each.apply(db, arguments);
|
||||
return await db.each(sql, ...params);
|
||||
}
|
||||
|
||||
async all<T>(sql: SqlType, ...params: any[]): Promise<T[]> {
|
||||
async all<T>(sql: SqlType, ...params: unknown[]): Promise<T[]> {
|
||||
const db = await this.db;
|
||||
return (await db.all<T[]>(sql, ...params));
|
||||
return await db.all<T[]>(sql, ...params);
|
||||
}
|
||||
|
||||
async exec(sql: SqlType, ...params: any[]): Promise<void> {
|
||||
async exec(sql: SqlType, ...params: unknown[]): Promise<void> {
|
||||
const db = await this.db;
|
||||
return await db.exec(sql, ...params);
|
||||
}
|
||||
|
||||
async prepare(sql: SqlType, ...params: any[]): Promise<Statement> {
|
||||
async prepare(sql: SqlType, ...params: unknown[]): Promise<Statement> {
|
||||
const db = await this.db;
|
||||
return await db.prepare(sql, ...params);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPatch(db: TypedDatabase, file: string): Promise<void> {
|
||||
Logger.tag('database', 'migration').info('Checking if patch need to be applied: %s', file);
|
||||
Logger.tag("database", "migration").info(
|
||||
"Checking if patch need to be applied: %s",
|
||||
file
|
||||
);
|
||||
|
||||
const contents = await pReadFile(file);
|
||||
const version = path.basename(file, '.sql');
|
||||
const version = path.basename(file, ".sql");
|
||||
|
||||
const row = await db.get('SELECT * FROM schema_version WHERE version = ?', version);
|
||||
const row = await db.get(
|
||||
"SELECT * FROM schema_version WHERE version = ?",
|
||||
version
|
||||
);
|
||||
if (row) {
|
||||
// patch is already applied. skip!
|
||||
Logger.tag('database', 'migration').info('Patch already applied, skipping: %s', file);
|
||||
return
|
||||
Logger.tag("database", "migration").info(
|
||||
"Patch already applied, skipping: %s",
|
||||
file
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = 'BEGIN TRANSACTION;\n' +
|
||||
contents.toString() + '\n' +
|
||||
'INSERT INTO schema_version (version) VALUES (\'' + version + '\');\n' +
|
||||
'END TRANSACTION;';
|
||||
const sql =
|
||||
"BEGIN TRANSACTION;\n" +
|
||||
contents.toString() +
|
||||
"\n" +
|
||||
"INSERT INTO schema_version (version) VALUES ('" +
|
||||
version +
|
||||
"');\n" +
|
||||
"END TRANSACTION;";
|
||||
|
||||
await db.exec(sql);
|
||||
|
||||
Logger.tag('database', 'migration').info('Patch successfully applied: %s', file);
|
||||
Logger.tag("database", "migration").info(
|
||||
"Patch successfully applied: %s",
|
||||
file
|
||||
);
|
||||
}
|
||||
|
||||
async function applyMigrations(db: TypedDatabase): Promise<void> {
|
||||
Logger.tag('database', 'migration').info('Migrating database...');
|
||||
Logger.tag("database", "migration").info("Migrating database...");
|
||||
|
||||
const sql = 'BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS schema_version (\n' +
|
||||
' version VARCHAR(255) PRIMARY KEY ASC,\n' +
|
||||
' applied_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL\n' +
|
||||
'); END TRANSACTION;';
|
||||
const sql =
|
||||
"BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS schema_version (\n" +
|
||||
" version VARCHAR(255) PRIMARY KEY ASC,\n" +
|
||||
" applied_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL\n" +
|
||||
"); END TRANSACTION;";
|
||||
|
||||
await db.exec(sql);
|
||||
|
||||
const files = await pglob(__dirname + '/patches/*.sql');
|
||||
const files = await pglob(__dirname + "/patches/*.sql");
|
||||
for (const file of files) {
|
||||
await applyPatch(db, file)
|
||||
await applyPatch(db, file);
|
||||
}
|
||||
}
|
||||
|
||||
export const db: TypedDatabase = new DatabasePromiseWrapper();
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
Logger.tag('database').info('Setting up database: %s', config.server.databaseFile);
|
||||
await db.on('profile', (sql: string, time: number) => Logger.tag('database').profile('[%sms]\t%s', time, sql));
|
||||
Logger.tag("database").info(
|
||||
"Setting up database: %s",
|
||||
config.server.databaseFile
|
||||
);
|
||||
await db.on("profile", (sql: string, time: number) =>
|
||||
Logger.tag("database").profile("[%sms]\t%s", time, sql)
|
||||
);
|
||||
|
||||
try {
|
||||
await applyMigrations(db);
|
||||
} catch (error) {
|
||||
Logger.tag('database').error('Error migrating database:', error);
|
||||
Logger.tag("database").error("Error migrating database:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
(function () {
|
||||
// Use graceful-fs instead of fs also in all libraries to have more robust fs handling.
|
||||
const realFs = require('fs');
|
||||
const gracefulFs = require('graceful-fs');
|
||||
gracefulFs.gracefulify(realFs);
|
||||
|
||||
// Init config by parsing commandline. Afterwards all other imports may happen.
|
||||
require('./config').parseCommandLine();
|
||||
})();
|
10
server/init.ts
Normal file
10
server/init.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import realFs from "fs";
|
||||
import gracefulFs from "graceful-fs";
|
||||
|
||||
import { parseCommandLine } from "./config";
|
||||
|
||||
// Use graceful-fs instead of fs also in all libraries to have more robust fs handling.
|
||||
gracefulFs.gracefulify(realFs);
|
||||
|
||||
// Init config by parsing commandline. Afterwards all other imports may happen.
|
||||
parseCommandLine();
|
|
@ -1,12 +1,13 @@
|
|||
import {fixNodeFilenames} from "../services/nodeService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
import { fixNodeFilenames } from "../services/nodeService";
|
||||
import { jobResultOkay } from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'FixNodeFilenamesJob',
|
||||
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
|
||||
name: "FixNodeFilenamesJob",
|
||||
description:
|
||||
"Makes sure node files (holding fastd key, name, etc.) are correctly named.",
|
||||
|
||||
async run() {
|
||||
await fixNodeFilenames();
|
||||
return jobResultOkay();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as MailService from "../services/mailService"
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
import * as MailService from "../services/mailService";
|
||||
import { jobResultOkay } from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'MailQueueJob',
|
||||
description: 'Send pending emails (up to 5 attempts in case of failures).',
|
||||
name: "MailQueueJob",
|
||||
description: "Send pending emails (up to 5 attempts in case of failures).",
|
||||
|
||||
async run() {
|
||||
await MailService.sendPendingMails();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
import { jobResultOkay } from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'MonitoringMailsSendingJob',
|
||||
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
|
||||
name: "MonitoringMailsSendingJob",
|
||||
description:
|
||||
"Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.",
|
||||
|
||||
async run() {
|
||||
await MonitoringService.sendMonitoringMails();
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay, jobResultWarning} from "./scheduler";
|
||||
import { jobResultOkay, jobResultWarning } from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'NodeInformationRetrievalJob',
|
||||
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
|
||||
name: "NodeInformationRetrievalJob",
|
||||
description:
|
||||
"Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.",
|
||||
|
||||
async run () {
|
||||
async run() {
|
||||
const result = await MonitoringService.retrieveNodeInformation();
|
||||
if (result.failedParsingNodesCount > 0) {
|
||||
return jobResultWarning(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
import { jobResultOkay } from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'OfflineNodesDeletionJob',
|
||||
description: 'Delete nodes that are offline for more than 100 days.',
|
||||
name: "OfflineNodesDeletionJob",
|
||||
description: "Delete nodes that are offline for more than 100 days.",
|
||||
|
||||
async run() {
|
||||
await MonitoringService.deleteOfflineNodes();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import cron from "node-cron";
|
||||
import moment from "moment";
|
||||
|
||||
import {config} from "../config";
|
||||
import { config } from "../config";
|
||||
import Logger from "../logger";
|
||||
|
||||
import MailQueueJob from "./MailQueueJob";
|
||||
|
@ -16,29 +16,29 @@ export enum JobResultState {
|
|||
}
|
||||
|
||||
export type JobResult = {
|
||||
state: JobResultState,
|
||||
message?: string,
|
||||
state: JobResultState;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export function jobResultOkay(message?: string): JobResult {
|
||||
return {
|
||||
state: JobResultState.OKAY,
|
||||
message
|
||||
}
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function jobResultWarning(message?: string): JobResult {
|
||||
return {
|
||||
state: JobResultState.WARNING,
|
||||
message
|
||||
}
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
name: string,
|
||||
description: string,
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
run(): Promise<JobResult>,
|
||||
run(): Promise<JobResult>;
|
||||
}
|
||||
|
||||
export enum TaskState {
|
||||
|
@ -59,7 +59,7 @@ export class Task {
|
|||
public lastRunDuration: number | null,
|
||||
public state: TaskState,
|
||||
public result: JobResult | null,
|
||||
public enabled: boolean,
|
||||
public enabled: boolean
|
||||
) {}
|
||||
|
||||
run(): void {
|
||||
|
@ -75,7 +75,7 @@ export class Task {
|
|||
const done = (state: TaskState, result: JobResult | null): void => {
|
||||
const now = moment();
|
||||
const duration = now.diff(this.runningSince || now);
|
||||
Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name);
|
||||
Logger.tag("jobs").profile("[%sms]\t%s", duration, this.name);
|
||||
|
||||
this.runningSince = null;
|
||||
this.lastRunDuration = duration;
|
||||
|
@ -83,16 +83,19 @@ export class Task {
|
|||
this.result = result;
|
||||
};
|
||||
|
||||
this.job.run().then(result => {
|
||||
done(TaskState.IDLE, result);
|
||||
}).catch(err => {
|
||||
Logger.tag('jobs').error("Job %s failed: %s", this.name, err);
|
||||
done(TaskState.FAILED, null);
|
||||
});
|
||||
this.job
|
||||
.run()
|
||||
.then((result) => {
|
||||
done(TaskState.IDLE, result);
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.tag("jobs").error("Job %s failed: %s", this.name, err);
|
||||
done(TaskState.FAILED, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type Tasks = {[key: string]: Task};
|
||||
type Tasks = { [key: string]: Task };
|
||||
|
||||
const tasks: Tasks = {};
|
||||
|
||||
|
@ -104,7 +107,7 @@ function nextTaskId(): number {
|
|||
}
|
||||
|
||||
function schedule(expr: string, job: Job): void {
|
||||
Logger.tag('jobs').info('Scheduling job: %s %s', expr, job.name);
|
||||
Logger.tag("jobs").info("Scheduling job: %s %s", expr, job.name);
|
||||
|
||||
const id = nextTaskId();
|
||||
|
||||
|
@ -119,33 +122,35 @@ function schedule(expr: string, job: Job): void {
|
|||
null,
|
||||
TaskState.IDLE,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
cron.schedule(expr, () => task.run());
|
||||
|
||||
tasks['' + id] = task;
|
||||
tasks["" + id] = task;
|
||||
}
|
||||
|
||||
export function init() {
|
||||
Logger.tag('jobs').info('Scheduling background jobs...');
|
||||
Logger.tag("jobs").info("Scheduling background jobs...");
|
||||
|
||||
try {
|
||||
schedule('0 */1 * * * *', MailQueueJob);
|
||||
schedule('15 */1 * * * *', FixNodeFilenamesJob);
|
||||
schedule("0 */1 * * * *", MailQueueJob);
|
||||
schedule("15 */1 * * * *", FixNodeFilenamesJob);
|
||||
|
||||
if (config.client.monitoring.enabled) {
|
||||
schedule('30 */15 * * * *', NodeInformationRetrievalJob);
|
||||
schedule('45 */5 * * * *', MonitoringMailsSendingJob);
|
||||
schedule('0 0 3 * * *', OfflineNodesDeletionJob); // every night at 3:00
|
||||
schedule("30 */15 * * * *", NodeInformationRetrievalJob);
|
||||
schedule("45 */5 * * * *", MonitoringMailsSendingJob);
|
||||
schedule("0 0 3 * * *", OfflineNodesDeletionJob); // every night at 3:00
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
Logger.tag('jobs').error('Error during scheduling of background jobs:', error);
|
||||
} catch (error) {
|
||||
Logger.tag("jobs").error(
|
||||
"Error during scheduling of background jobs:",
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Logger.tag('jobs').info('Scheduling of background jobs done.');
|
||||
Logger.tag("jobs").info("Scheduling of background jobs done.");
|
||||
}
|
||||
|
||||
export function getTasks(): Tasks {
|
||||
|
|
|
@ -1,46 +1,48 @@
|
|||
import {isLogLevel, isUndefined, LoggingConfig, LogLevel, LogLevels} from "./types";
|
||||
import {ActivatableLoggerImpl} from "./logger";
|
||||
import {
|
||||
isLogLevel,
|
||||
isUndefined,
|
||||
LoggingConfig,
|
||||
LogLevel,
|
||||
LogLevels,
|
||||
} from "./types";
|
||||
import { ActivatableLoggerImpl } from "./logger";
|
||||
|
||||
function withDefault<T>(value: T | undefined, defaultValue: T): T {
|
||||
return isUndefined(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
class TestableLogger extends ActivatableLoggerImpl {
|
||||
private logs: any[][] = [];
|
||||
private logs: unknown[][] = [];
|
||||
|
||||
constructor(
|
||||
enabled?: boolean,
|
||||
debug?: boolean,
|
||||
profile?: boolean,
|
||||
) {
|
||||
constructor(enabled?: boolean, debug?: boolean, profile?: boolean) {
|
||||
super();
|
||||
this.init(
|
||||
new LoggingConfig(
|
||||
withDefault(enabled, true),
|
||||
withDefault(debug, true),
|
||||
withDefault(profile, true),
|
||||
withDefault(profile, true)
|
||||
),
|
||||
(...args: any[]): void => this.doLog(...args)
|
||||
(...args: unknown[]): void => this.doLog(...args)
|
||||
);
|
||||
}
|
||||
|
||||
doLog(...args: any[]): void {
|
||||
doLog(...args: unknown[]): void {
|
||||
this.logs.push(args);
|
||||
}
|
||||
|
||||
getLogs(): any[][] {
|
||||
getLogs(): unknown[][] {
|
||||
return this.logs;
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedLogEntry = {
|
||||
level: LogLevel,
|
||||
tags: string[],
|
||||
message: string,
|
||||
args: any[],
|
||||
level: LogLevel;
|
||||
tags: string[];
|
||||
message: string;
|
||||
args: unknown[];
|
||||
};
|
||||
|
||||
function parseLogEntry(logEntry: any[]): ParsedLogEntry {
|
||||
function parseLogEntry(logEntry: unknown[]): ParsedLogEntry {
|
||||
if (!logEntry.length) {
|
||||
throw new Error(
|
||||
`Empty log entry. Should always start with log message: ${logEntry}`
|
||||
|
@ -55,7 +57,8 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry {
|
|||
}
|
||||
|
||||
// noinspection RegExpRedundantEscape
|
||||
const regexp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ([A-Z]+) - (\[[^\]]*\])? *(.*)$/;
|
||||
const regexp =
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ([A-Z]+) - (\[[^\]]*\])? *(.*)$/;
|
||||
const groups = logMessage.match(regexp);
|
||||
if (groups === null || groups.length < 4) {
|
||||
throw new Error(
|
||||
|
@ -71,7 +74,7 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry {
|
|||
}
|
||||
|
||||
const tagsStr = groups[2].substring(1, groups[2].length - 1);
|
||||
const tags = tagsStr ? tagsStr.split(", "): [];
|
||||
const tags = tagsStr ? tagsStr.split(", ") : [];
|
||||
const message = groups[3];
|
||||
const args = logEntry.slice(1);
|
||||
|
||||
|
@ -83,7 +86,7 @@ function parseLogEntry(logEntry: any[]): ParsedLogEntry {
|
|||
};
|
||||
}
|
||||
|
||||
function parseLogs(logs: any[][]): ParsedLogEntry[] {
|
||||
function parseLogs(logs: unknown[][]): ParsedLogEntry[] {
|
||||
const parsedLogs: ParsedLogEntry[] = [];
|
||||
for (const logEntry of logs) {
|
||||
parsedLogs.push(parseLogEntry(logEntry));
|
||||
|
@ -100,12 +103,14 @@ for (const level of LogLevels) {
|
|||
logger.tag()[level]("message");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: [],
|
||||
message: "message",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: [],
|
||||
message: "message",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should log single tagged ${level} message without parameters`, () => {
|
||||
|
@ -116,12 +121,14 @@ for (const level of LogLevels) {
|
|||
logger.tag("tag1", "tag2")[level]("message");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: ["tag1", "tag2"],
|
||||
message: "message",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: ["tag1", "tag2"],
|
||||
message: "message",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should log single tagged ${level} message with parameters`, () => {
|
||||
|
@ -132,12 +139,14 @@ for (const level of LogLevels) {
|
|||
logger.tag("tag1", "tag2")[level]("message", 1, {}, [false]);
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: ["tag1", "tag2"],
|
||||
message: "message",
|
||||
args: [1, {}, [false]],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: ["tag1", "tag2"],
|
||||
message: "message",
|
||||
args: [1, {}, [false]],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should escape tags for ${level} message without parameters`, () => {
|
||||
|
@ -148,12 +157,14 @@ for (const level of LogLevels) {
|
|||
logger.tag("%s", "%d", "%f", "%o", "%")[level]("message");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: ["%%s", "%%d", "%%f", "%%o", "%%"],
|
||||
message: "message",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: ["%%s", "%%d", "%%f", "%%o", "%%"],
|
||||
message: "message",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should not escape ${level} message itself`, () => {
|
||||
|
@ -164,12 +175,14 @@ for (const level of LogLevels) {
|
|||
logger.tag("tag")[level]("%s %d %f %o %%");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: ["tag"],
|
||||
message: "%s %d %f %o %%",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: ["tag"],
|
||||
message: "%s %d %f %o %%",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should not escape ${level} message arguments`, () => {
|
||||
|
@ -180,12 +193,14 @@ for (const level of LogLevels) {
|
|||
logger.tag("tag")[level]("message", 1, "%s", "%d", "%f", "%o", "%");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level,
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [1, "%s", "%d", "%f", "%o", "%"],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level,
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [1, "%s", "%d", "%f", "%o", "%"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should not log ${level} message on disabled logger`, () => {
|
||||
|
@ -219,12 +234,14 @@ test(`should log profile message with disabled debugging`, () => {
|
|||
logger.tag("tag").profile("message");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level: "profile",
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level: "profile",
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test(`should not log profile message with disabled profiling`, () => {
|
||||
|
@ -246,10 +263,12 @@ test(`should log debug message with disabled profiling`, () => {
|
|||
logger.tag("tag").debug("message");
|
||||
|
||||
// then
|
||||
expect(parseLogs(logger.getLogs())).toEqual([{
|
||||
level: "debug",
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [],
|
||||
}]);
|
||||
expect(parseLogs(logger.getLogs())).toEqual([
|
||||
{
|
||||
level: "debug",
|
||||
tags: ["tag"],
|
||||
message: "message",
|
||||
args: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -1,15 +1,28 @@
|
|||
import {isString, Logger, LoggingConfig, LogLevel, TaggedLogger} from './types';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
isString,
|
||||
Logger,
|
||||
LoggingConfig,
|
||||
LogLevel,
|
||||
TaggedLogger,
|
||||
} from "./types";
|
||||
import moment from "moment";
|
||||
|
||||
export type LoggingFunction = (...args: any[]) => void;
|
||||
export type LoggingFunction = (...args: unknown[]) => void;
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
const noopTaggedLogger: TaggedLogger = {
|
||||
log(_level: LogLevel, ..._args: any[]): void {},
|
||||
debug(..._args: any[]): void {},
|
||||
info(..._args: any[]): void {},
|
||||
warn(..._args: any[]): void {},
|
||||
error(..._args: any[]): void {},
|
||||
profile(..._args: any[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
log(level: LogLevel, ...args: unknown[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
debug(...args: unknown[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
info(...args: unknown[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
warn(...args: unknown[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
error(...args: unknown[]): void {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
|
||||
profile(...args: unknown[]): void {},
|
||||
};
|
||||
|
||||
export interface ActivatableLogger extends Logger {
|
||||
|
@ -34,17 +47,20 @@ export class ActivatableLoggerImpl implements ActivatableLogger {
|
|||
const profile = this.config.profile;
|
||||
const loggingFunction = this.loggingFunction;
|
||||
return {
|
||||
log(level: LogLevel, ...args: any[]): void {
|
||||
const timeStr = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||
log(level: LogLevel, ...args: unknown[]): void {
|
||||
const timeStr = moment().format("YYYY-MM-DD HH:mm:ss");
|
||||
const levelStr = level.toUpperCase();
|
||||
const tagsStr = tags ? '[' + tags.join(', ') + ']' : '';
|
||||
const tagsStr = tags ? "[" + tags.join(", ") + "]" : "";
|
||||
const messagePrefix = `${timeStr} ${levelStr} - ${tagsStr}`;
|
||||
|
||||
// Make sure to only replace %s, etc. in real log message
|
||||
// but not in tags.
|
||||
const escapedMessagePrefix = messagePrefix.replace(/%/g, '%%');
|
||||
const escapedMessagePrefix = messagePrefix.replace(
|
||||
/%/g,
|
||||
"%%"
|
||||
);
|
||||
|
||||
let message = '';
|
||||
let message = "";
|
||||
if (args && isString(args[0])) {
|
||||
message = args[0];
|
||||
args.shift();
|
||||
|
@ -55,26 +71,26 @@ export class ActivatableLoggerImpl implements ActivatableLogger {
|
|||
: escapedMessagePrefix;
|
||||
loggingFunction(logStr, ...args);
|
||||
},
|
||||
debug(...args: any[]): void {
|
||||
debug(...args: unknown[]): void {
|
||||
if (debug) {
|
||||
this.log('debug', ...args);
|
||||
this.log("debug", ...args);
|
||||
}
|
||||
},
|
||||
info(...args: any[]): void {
|
||||
this.log('info', ...args);
|
||||
info(...args: unknown[]): void {
|
||||
this.log("info", ...args);
|
||||
},
|
||||
warn(...args: any[]): void {
|
||||
this.log('warn', ...args);
|
||||
warn(...args: unknown[]): void {
|
||||
this.log("warn", ...args);
|
||||
},
|
||||
error(...args: any[]): void {
|
||||
this.log('error', ...args);
|
||||
error(...args: unknown[]): void {
|
||||
this.log("error", ...args);
|
||||
},
|
||||
profile(...args: any[]): void {
|
||||
profile(...args: unknown[]): void {
|
||||
if (profile) {
|
||||
this.log('profile', ...args);
|
||||
this.log("profile", ...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return noopTaggedLogger;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {createTransport, Transporter} from "nodemailer";
|
||||
import {config} from "../config";
|
||||
import { createTransport, Transporter } from "nodemailer";
|
||||
import { config } from "../config";
|
||||
import * as MailTemplateService from "../services/mailTemplateService";
|
||||
import Mail from "nodemailer/lib/mailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
import "./init"
|
||||
import {config} from "./config"
|
||||
import Logger from "./logger"
|
||||
import * as db from "./db/database"
|
||||
import * as scheduler from "./jobs/scheduler"
|
||||
import * as router from "./router"
|
||||
import * as app from "./app"
|
||||
import "./init";
|
||||
import { config } from "./config";
|
||||
import Logger from "./logger";
|
||||
import * as db from "./db/database";
|
||||
import * as scheduler from "./jobs/scheduler";
|
||||
import * as router from "./router";
|
||||
import * as app from "./app";
|
||||
import * as mail from "./mail";
|
||||
|
||||
app.init();
|
||||
Logger.init(config.server.logging);
|
||||
Logger.tag('main', 'startup').info('Server starting up...');
|
||||
Logger.tag("main", "startup").info("Server starting up...");
|
||||
|
||||
async function main() {
|
||||
Logger.tag('main').info('Initializing...');
|
||||
Logger.tag("main").info("Initializing...");
|
||||
|
||||
await db.init();
|
||||
mail.init();
|
||||
scheduler.init();
|
||||
router.init();
|
||||
|
||||
app.app.listen(config.server.port, '::');
|
||||
app.app.listen(config.server.port, "::");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(error => {
|
||||
console.error('Unhandled runtime error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch((error) => {
|
||||
console.error("Unhandled runtime error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {handleJSON} from "../utils/resources";
|
||||
import {config} from "../config";
|
||||
import { handleJSON } from "../utils/resources";
|
||||
import { config } from "../config";
|
||||
|
||||
export const get = handleJSON(async () => config.client);
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import {promises as fs} from "graceful-fs";
|
||||
import { promises as fs } from "graceful-fs";
|
||||
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import Logger from "../logger";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {Request, Response} from "express";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
const indexHtml = __dirname + '/../../client/index.html';
|
||||
const indexHtml = __dirname + "/../../client/index.html";
|
||||
|
||||
export function render (req: Request, res: Response): void {
|
||||
export function render(req: Request, res: Response): void {
|
||||
const data = Resources.getData(req);
|
||||
|
||||
fs.readFile(indexHtml, 'utf8')
|
||||
.then(body =>
|
||||
fs.readFile(indexHtml, "utf8")
|
||||
.then((body) =>
|
||||
Resources.successHtml(
|
||||
res,
|
||||
body.replace(
|
||||
/<body/,
|
||||
'<script>window.__nodeToken = \''+ data.token + '\';</script><body'
|
||||
"<script>window.__nodeToken = '" +
|
||||
data.token +
|
||||
"';</script><body"
|
||||
)
|
||||
))
|
||||
.catch(err => {
|
||||
Logger.tag('frontend').error('Could not read file: ', indexHtml, err);
|
||||
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
|
||||
})
|
||||
)
|
||||
)
|
||||
.catch((err) => {
|
||||
Logger.tag("frontend").error(
|
||||
"Could not read file: ",
|
||||
indexHtml,
|
||||
err
|
||||
);
|
||||
return Resources.error(res, {
|
||||
data: "Internal error.",
|
||||
type: ErrorTypes.internalError,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,53 +2,55 @@ import CONSTRAINTS from "../shared/validation/constraints";
|
|||
import ErrorTypes from "../utils/errorTypes";
|
||||
import * as MailService from "../services/mailService";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {handleJSONWithData, RequestData} from "../utils/resources";
|
||||
import {normalizeString, parseInteger} from "../shared/utils/strings";
|
||||
import {forConstraint} from "../shared/validation/validator";
|
||||
import {Request, Response} from "express";
|
||||
import {isString, Mail, MailId} from "../types";
|
||||
import { handleJSONWithData, RequestData } from "../utils/resources";
|
||||
import { normalizeString, parseInteger } from "../shared/utils/strings";
|
||||
import { forConstraint } from "../shared/validation/validator";
|
||||
import { Request, Response } from "express";
|
||||
import { isString, Mail, MailId } from "../types";
|
||||
|
||||
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
||||
|
||||
async function withValidMailId(data: RequestData): Promise<MailId> {
|
||||
if (!isString(data.id)) {
|
||||
throw {data: 'Missing mail id.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Missing mail id.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
const id = normalizeString(data.id);
|
||||
|
||||
if (!isValidId(id)) {
|
||||
throw {data: 'Invalid mail id.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid mail id.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
return parseInteger(id) as MailId;
|
||||
}
|
||||
|
||||
export const get = handleJSONWithData(async data => {
|
||||
export const get = handleJSONWithData(async (data) => {
|
||||
const id = await withValidMailId(data);
|
||||
return await MailService.getMail(id);
|
||||
});
|
||||
|
||||
async function doGetAll(req: Request): Promise<{ total: number, mails: Mail[] }> {
|
||||
const restParams = await Resources.getValidRestParams('list', null, req);
|
||||
async function doGetAll(
|
||||
req: Request
|
||||
): Promise<{ total: number; mails: Mail[] }> {
|
||||
const restParams = await Resources.getValidRestParams("list", null, req);
|
||||
return await MailService.getPendingMails(restParams);
|
||||
}
|
||||
|
||||
export function getAll(req: Request, res: Response): void {
|
||||
doGetAll(req)
|
||||
.then(({total, mails}) => {
|
||||
res.set('X-Total-Count', total.toString(10));
|
||||
.then(({ total, mails }) => {
|
||||
res.set("X-Total-Count", total.toString(10));
|
||||
return Resources.success(res, mails);
|
||||
})
|
||||
.catch(err => Resources.error(res, err))
|
||||
.catch((err) => Resources.error(res, err));
|
||||
}
|
||||
|
||||
export const remove = handleJSONWithData(async data => {
|
||||
export const remove = handleJSONWithData(async (data) => {
|
||||
const id = await withValidMailId(data);
|
||||
await MailService.deleteMail(id);
|
||||
});
|
||||
|
||||
export const resetFailures = handleJSONWithData(async data => {
|
||||
export const resetFailures = handleJSONWithData(async (data) => {
|
||||
const id = await withValidMailId(data);
|
||||
return await MailService.resetFailures(id);
|
||||
});
|
||||
|
|
|
@ -2,55 +2,63 @@ import CONSTRAINTS from "../shared/validation/constraints";
|
|||
import ErrorTypes from "../utils/errorTypes";
|
||||
import * as MonitoringService from "../services/monitoringService";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {handleJSONWithData} from "../utils/resources";
|
||||
import {normalizeString} from "../shared/utils/strings";
|
||||
import {forConstraint} from "../shared/validation/validator";
|
||||
import {Request, Response} from "express";
|
||||
import {isMonitoringToken, JSONObject, MonitoringResponse, MonitoringToken, toMonitoringResponse} from "../types";
|
||||
import { handleJSONWithData } from "../utils/resources";
|
||||
import { normalizeString } from "../shared/utils/strings";
|
||||
import { forConstraint } from "../shared/validation/validator";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
isMonitoringToken,
|
||||
JSONObject,
|
||||
MonitoringResponse,
|
||||
MonitoringToken,
|
||||
toMonitoringResponse,
|
||||
} from "../types";
|
||||
|
||||
const isValidToken = forConstraint(CONSTRAINTS.token, false);
|
||||
|
||||
// FIXME: Get rid of any
|
||||
async function doGetAll(req: Request): Promise<{ total: number, result: any }> {
|
||||
const restParams = await Resources.getValidRestParams('list', null, req);
|
||||
const {monitoringStates, total} = await MonitoringService.getAll(restParams);
|
||||
async function doGetAll(req: Request): Promise<{ total: number; result: any }> {
|
||||
const restParams = await Resources.getValidRestParams("list", null, req);
|
||||
const { monitoringStates, total } = await MonitoringService.getAll(
|
||||
restParams
|
||||
);
|
||||
return {
|
||||
total,
|
||||
result: monitoringStates.map(state => {
|
||||
result: monitoringStates.map((state) => {
|
||||
state.mapId = state.mac.toLowerCase().replace(/:/g, "");
|
||||
return state;
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAll(req: Request, res: Response): void {
|
||||
doGetAll(req)
|
||||
.then(({total, result}) => {
|
||||
res.set('X-Total-Count', total.toString(10));
|
||||
Resources.success(res, result)
|
||||
.then(({ total, result }) => {
|
||||
res.set("X-Total-Count", total.toString(10));
|
||||
Resources.success(res, result);
|
||||
})
|
||||
.catch(err => Resources.error(res, err));
|
||||
.catch((err) => Resources.error(res, err));
|
||||
}
|
||||
|
||||
function getValidatedToken(data: JSONObject): MonitoringToken {
|
||||
if (!isMonitoringToken(data.token)) {
|
||||
throw {data: 'Missing token.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Missing token.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
const token = normalizeString(data.token);
|
||||
if (!isValidToken(token)) {
|
||||
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid token.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
return token as MonitoringToken;
|
||||
}
|
||||
|
||||
export const confirm = handleJSONWithData<MonitoringResponse>(async data => {
|
||||
export const confirm = handleJSONWithData<MonitoringResponse>(async (data) => {
|
||||
const validatedToken = getValidatedToken(data);
|
||||
|
||||
const node = await MonitoringService.confirm(validatedToken);
|
||||
return toMonitoringResponse(node);
|
||||
});
|
||||
|
||||
export const disable = handleJSONWithData<MonitoringResponse>(async data => {
|
||||
export const disable = handleJSONWithData<MonitoringResponse>(async (data) => {
|
||||
const validatedToken: MonitoringToken = getValidatedToken(data);
|
||||
|
||||
const node = await MonitoringService.disable(validatedToken);
|
||||
|
|
|
@ -2,14 +2,15 @@ import Constraints from "../shared/validation/constraints";
|
|||
import ErrorTypes from "../utils/errorTypes";
|
||||
import * as MonitoringService from "../services/monitoringService";
|
||||
import * as NodeService from "../services/nodeService";
|
||||
import {normalizeMac, normalizeString} from "../shared/utils/strings";
|
||||
import {forConstraint, forConstraints} from "../shared/validation/validator";
|
||||
import { normalizeMac, normalizeString } from "../shared/utils/strings";
|
||||
import { forConstraint, forConstraints } from "../shared/validation/validator";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {handleJSONWithData} from "../utils/resources";
|
||||
import {Request, Response} from "express";
|
||||
import { handleJSONWithData } from "../utils/resources";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
CreateOrUpdateNode,
|
||||
DomainSpecificNodeResponse,
|
||||
filterUndefinedFromJSON,
|
||||
isCreateOrUpdateNode,
|
||||
isNodeSortField,
|
||||
isString,
|
||||
|
@ -24,18 +25,26 @@ import {
|
|||
toDomainSpecificNodeResponse,
|
||||
Token,
|
||||
toNodeResponse,
|
||||
toNodeTokenResponse
|
||||
toNodeTokenResponse,
|
||||
} from "../types";
|
||||
|
||||
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
|
||||
const nodeFields = [
|
||||
"hostname",
|
||||
"key",
|
||||
"email",
|
||||
"nickname",
|
||||
"mac",
|
||||
"coords",
|
||||
"monitoring",
|
||||
];
|
||||
|
||||
function getNormalizedNodeData(reqData: JSONObject): CreateOrUpdateNode {
|
||||
const node: { [key: string]: any } = {};
|
||||
const node: { [key: string]: unknown } = {};
|
||||
for (const field of nodeFields) {
|
||||
let value: JSONValue | undefined = reqData[field];
|
||||
if (isString(value)) {
|
||||
value = normalizeString(value);
|
||||
if (field === 'mac') {
|
||||
if (field === "mac") {
|
||||
value = normalizeMac(value as MAC);
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +58,7 @@ function getNormalizedNodeData(reqData: JSONObject): CreateOrUpdateNode {
|
|||
return node;
|
||||
}
|
||||
|
||||
throw {data: "Invalid node data.", type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid node data.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
const isValidNode = forConstraints(Constraints.node, false);
|
||||
|
@ -57,77 +66,82 @@ const isValidToken = forConstraint(Constraints.token, false);
|
|||
|
||||
function getValidatedToken(data: JSONObject): Token {
|
||||
if (!isToken(data.token)) {
|
||||
throw {data: 'Missing token.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Missing token.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
const token = normalizeString(data.token);
|
||||
if (!isValidToken(token)) {
|
||||
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid token.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
return token as Token;
|
||||
}
|
||||
|
||||
export const create = handleJSONWithData<NodeTokenResponse>(async data => {
|
||||
export const create = handleJSONWithData<NodeTokenResponse>(async (data) => {
|
||||
const baseNode = getNormalizedNodeData(data);
|
||||
if (!isValidNode(baseNode)) {
|
||||
throw {data: 'Invalid node data.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid node data.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
const node = await NodeService.createNode(baseNode);
|
||||
return toNodeTokenResponse(node);
|
||||
});
|
||||
|
||||
export const update = handleJSONWithData<NodeTokenResponse>(async data => {
|
||||
export const update = handleJSONWithData<NodeTokenResponse>(async (data) => {
|
||||
const validatedToken: Token = getValidatedToken(data);
|
||||
const baseNode = getNormalizedNodeData(data);
|
||||
if (!isValidNode(baseNode)) {
|
||||
throw {data: 'Invalid node data.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid node data.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
const node = await NodeService.updateNode(validatedToken, baseNode);
|
||||
return toNodeTokenResponse(node);
|
||||
});
|
||||
|
||||
export const remove = handleJSONWithData<void>(async data => {
|
||||
export const remove = handleJSONWithData<void>(async (data) => {
|
||||
const validatedToken = getValidatedToken(data);
|
||||
await NodeService.deleteNode(validatedToken);
|
||||
});
|
||||
|
||||
export const get = handleJSONWithData<NodeResponse>(async data => {
|
||||
export const get = handleJSONWithData<NodeResponse>(async (data) => {
|
||||
const validatedToken: Token = getValidatedToken(data);
|
||||
const node = await NodeService.getNodeDataByToken(validatedToken);
|
||||
return toNodeResponse(node);
|
||||
});
|
||||
|
||||
async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }> {
|
||||
const restParams = await Resources.getValidRestParams('list', 'node', req);
|
||||
async function doGetAll(
|
||||
req: Request
|
||||
): Promise<{ total: number; pageNodes: DomainSpecificNodeResponse[] }> {
|
||||
const restParams = await Resources.getValidRestParams("list", "node", req);
|
||||
|
||||
const nodes = await NodeService.getAllNodes();
|
||||
|
||||
const realNodes = nodes.filter(node =>
|
||||
// We ignore nodes without tokens as those are only manually added ones like gateways.
|
||||
!!node.token // FIXME: As node.token may not be undefined or null here, handle this when loading!
|
||||
const realNodes = nodes.filter(
|
||||
(node) =>
|
||||
// We ignore nodes without tokens as those are only manually added ones like gateways.
|
||||
!!node.token // FIXME: As node.token may not be undefined or null here, handle this when loading!
|
||||
);
|
||||
|
||||
const macs: MAC[] = realNodes.map(node => node.mac);
|
||||
const macs: MAC[] = realNodes.map((node) => node.mac);
|
||||
const nodeStateByMac = await MonitoringService.getByMacs(macs);
|
||||
|
||||
const domainSpecificNodes: DomainSpecificNodeResponse[] = realNodes.map(node => {
|
||||
const nodeState: NodeStateData = nodeStateByMac[node.mac] || {};
|
||||
return toDomainSpecificNodeResponse(node, nodeState);
|
||||
});
|
||||
const domainSpecificNodes: DomainSpecificNodeResponse[] = realNodes.map(
|
||||
(node) => {
|
||||
const nodeState: NodeStateData = nodeStateByMac[node.mac] || {};
|
||||
return toDomainSpecificNodeResponse(node, nodeState);
|
||||
}
|
||||
);
|
||||
|
||||
const filteredNodes = Resources.filter<DomainSpecificNodeResponse>(
|
||||
domainSpecificNodes,
|
||||
[
|
||||
'hostname',
|
||||
'nickname',
|
||||
'email',
|
||||
'token',
|
||||
'mac',
|
||||
'site',
|
||||
'domain',
|
||||
'key',
|
||||
'onlineState'
|
||||
"hostname",
|
||||
"nickname",
|
||||
"email",
|
||||
"token",
|
||||
"mac",
|
||||
"site",
|
||||
"domain",
|
||||
"key",
|
||||
"onlineState",
|
||||
],
|
||||
restParams
|
||||
);
|
||||
|
@ -141,14 +155,22 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
|
|||
);
|
||||
const pageNodes = Resources.getPageEntities(sortedNodes, restParams);
|
||||
|
||||
return {total, pageNodes};
|
||||
return { total, pageNodes };
|
||||
}
|
||||
|
||||
export function getAll(req: Request, res: Response): void {
|
||||
doGetAll(req)
|
||||
.then((result: { total: number, pageNodes: any[] }) => {
|
||||
res.set('X-Total-Count', result.total.toString(10));
|
||||
return Resources.success(res, result.pageNodes);
|
||||
})
|
||||
.catch((err: any) => Resources.error(res, err));
|
||||
.then(
|
||||
(result: {
|
||||
total: number;
|
||||
pageNodes: DomainSpecificNodeResponse[];
|
||||
}) => {
|
||||
res.set("X-Total-Count", result.total.toString(10));
|
||||
return Resources.success(
|
||||
res,
|
||||
result.pageNodes.map(filterUndefinedFromJSON)
|
||||
);
|
||||
}
|
||||
)
|
||||
.catch((err) => Resources.error(res, err));
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import ErrorTypes from "../utils/errorTypes";
|
||||
import Logger from "../logger";
|
||||
import {getNodeStatistics} from "../services/nodeService";
|
||||
import {handleJSON} from "../utils/resources";
|
||||
import { getNodeStatistics } from "../services/nodeService";
|
||||
import { handleJSON } from "../utils/resources";
|
||||
|
||||
export const get = handleJSON(async () => {
|
||||
try {
|
||||
const nodeStatistics = await getNodeStatistics();
|
||||
return {
|
||||
nodes: nodeStatistics
|
||||
nodes: nodeStatistics,
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.tag('statistics').error('Error getting statistics:', error);
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
Logger.tag("statistics").error("Error getting statistics:", error);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import CONSTRAINTS from "../shared/validation/constraints";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {handleJSONWithData, RequestData} from "../utils/resources";
|
||||
import {getTasks, Task, TaskState} from "../jobs/scheduler";
|
||||
import {normalizeString} from "../shared/utils/strings";
|
||||
import {forConstraint} from "../shared/validation/validator";
|
||||
import {Request, Response} from "express";
|
||||
import {isString, isTaskSortField} from "../types";
|
||||
import { handleJSONWithData, RequestData } from "../utils/resources";
|
||||
import { getTasks, Task, TaskState } from "../jobs/scheduler";
|
||||
import { normalizeString } from "../shared/utils/strings";
|
||||
import { forConstraint } from "../shared/validation/validator";
|
||||
import { Request, Response } from "express";
|
||||
import { isString, isTaskSortField } from "../types";
|
||||
|
||||
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
||||
|
||||
|
@ -22,7 +22,7 @@ type TaskResponse = {
|
|||
result: string | null;
|
||||
message: string | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
function toTaskResponse(task: Task): TaskResponse {
|
||||
return {
|
||||
|
@ -34,20 +34,26 @@ function toTaskResponse(task: Task): TaskResponse {
|
|||
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
|
||||
lastRunDuration: task.lastRunDuration || null,
|
||||
state: task.state,
|
||||
result: task.state !== TaskState.RUNNING && task.result ? task.result.state : null,
|
||||
message: task.state !== TaskState.RUNNING && task.result ? task.result.message || null : null,
|
||||
enabled: task.enabled
|
||||
result:
|
||||
task.state !== TaskState.RUNNING && task.result
|
||||
? task.result.state
|
||||
: null,
|
||||
message:
|
||||
task.state !== TaskState.RUNNING && task.result
|
||||
? task.result.message || null
|
||||
: null,
|
||||
enabled: task.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
async function withValidTaskId(data: RequestData): Promise<string> {
|
||||
if (!isString(data.id)) {
|
||||
throw {data: 'Missing task id.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Missing task id.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
const id = normalizeString(data.id);
|
||||
|
||||
if (!isValidId(id)) {
|
||||
throw {data: 'Invalid task id.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid task id.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
return id;
|
||||
|
@ -58,7 +64,7 @@ async function getTask(id: string): Promise<Task> {
|
|||
const task = tasks[id];
|
||||
|
||||
if (!task) {
|
||||
throw {data: 'Task not found.', type: ErrorTypes.notFound};
|
||||
throw { data: "Task not found.", type: ErrorTypes.notFound };
|
||||
}
|
||||
|
||||
return task;
|
||||
|
@ -69,14 +75,19 @@ async function withTask(data: RequestData): Promise<Task> {
|
|||
return await getTask(id);
|
||||
}
|
||||
|
||||
async function setTaskEnabled(data: RequestData, enable: boolean): Promise<TaskResponse> {
|
||||
async function setTaskEnabled(
|
||||
data: RequestData,
|
||||
enable: boolean
|
||||
): Promise<TaskResponse> {
|
||||
const task = await withTask(data);
|
||||
task.enabled = enable;
|
||||
return toTaskResponse(task);
|
||||
}
|
||||
|
||||
async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Task[] }> {
|
||||
const restParams = await Resources.getValidRestParams('list', null, req);
|
||||
async function doGetAll(
|
||||
req: Request
|
||||
): Promise<{ total: number; pageTasks: Task[] }> {
|
||||
const restParams = await Resources.getValidRestParams("list", null, req);
|
||||
|
||||
const tasks = Resources.sort(
|
||||
Object.values(getTasks()),
|
||||
|
@ -85,7 +96,7 @@ async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Task[
|
|||
);
|
||||
const filteredTasks = Resources.filter(
|
||||
tasks,
|
||||
['id', 'name', 'schedule', 'state'],
|
||||
["id", "name", "schedule", "state"],
|
||||
restParams
|
||||
);
|
||||
|
||||
|
@ -100,28 +111,28 @@ async function doGetAll(req: Request): Promise<{ total: number, pageTasks: Task[
|
|||
|
||||
export function getAll(req: Request, res: Response): void {
|
||||
doGetAll(req)
|
||||
.then(({total, pageTasks}) => {
|
||||
res.set('X-Total-Count', total.toString(10));
|
||||
.then(({ total, pageTasks }) => {
|
||||
res.set("X-Total-Count", total.toString(10));
|
||||
Resources.success(res, pageTasks.map(toTaskResponse));
|
||||
})
|
||||
.catch(err => Resources.error(res, err));
|
||||
.catch((err) => Resources.error(res, err));
|
||||
}
|
||||
|
||||
export const run = handleJSONWithData(async data => {
|
||||
export const run = handleJSONWithData(async (data) => {
|
||||
const task = await withTask(data);
|
||||
|
||||
if (task.runningSince) {
|
||||
throw {data: 'Task already running.', type: ErrorTypes.conflict};
|
||||
throw { data: "Task already running.", type: ErrorTypes.conflict };
|
||||
}
|
||||
|
||||
task.run();
|
||||
return toTaskResponse(task);
|
||||
});
|
||||
|
||||
export const enable = handleJSONWithData(async data => {
|
||||
export const enable = handleJSONWithData(async (data) => {
|
||||
await setTaskEnabled(data, true);
|
||||
});
|
||||
|
||||
export const disable = handleJSONWithData(async data => {
|
||||
export const disable = handleJSONWithData(async (data) => {
|
||||
await setTaskEnabled(data, false);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {handleJSON} from "../utils/resources";
|
||||
import {version} from "../config";
|
||||
import { handleJSON } from "../utils/resources";
|
||||
import { version } from "../config";
|
||||
|
||||
export const get = handleJSON(async () => ({
|
||||
version
|
||||
version,
|
||||
}));
|
||||
|
|
|
@ -1,51 +1,51 @@
|
|||
import express from "express"
|
||||
import express from "express";
|
||||
|
||||
import {app} from "./app"
|
||||
import {config} from "./config"
|
||||
import { app } from "./app";
|
||||
import { config } from "./config";
|
||||
|
||||
import * as ConfigResource from "./resources/configResource"
|
||||
import * as VersionResource from "./resources/versionResource"
|
||||
import * as StatisticsResource from "./resources/statisticsResource"
|
||||
import * as FrontendResource from "./resources/frontendResource"
|
||||
import * as NodeResource from "./resources/nodeResource"
|
||||
import * as MonitoringResource from "./resources/monitoringResource"
|
||||
import * as TaskResource from "./resources/taskResource"
|
||||
import * as MailResource from "./resources/mailResource"
|
||||
import * as ConfigResource from "./resources/configResource";
|
||||
import * as VersionResource from "./resources/versionResource";
|
||||
import * as StatisticsResource from "./resources/statisticsResource";
|
||||
import * as FrontendResource from "./resources/frontendResource";
|
||||
import * as NodeResource from "./resources/nodeResource";
|
||||
import * as MonitoringResource from "./resources/monitoringResource";
|
||||
import * as TaskResource from "./resources/taskResource";
|
||||
import * as MailResource from "./resources/mailResource";
|
||||
|
||||
export function init (): void {
|
||||
export function init(): void {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', FrontendResource.render);
|
||||
router.post("/", FrontendResource.render);
|
||||
|
||||
router.get('/api/config', ConfigResource.get);
|
||||
router.get('/api/version', VersionResource.get);
|
||||
router.get("/api/config", ConfigResource.get);
|
||||
router.get("/api/version", VersionResource.get);
|
||||
|
||||
router.post('/api/node', NodeResource.create);
|
||||
router.put('/api/node/:token', NodeResource.update);
|
||||
router.delete('/api/node/:token', NodeResource.remove);
|
||||
router.get('/api/node/:token', NodeResource.get);
|
||||
router.post("/api/node", NodeResource.create);
|
||||
router.put("/api/node/:token", NodeResource.update);
|
||||
router.delete("/api/node/:token", NodeResource.remove);
|
||||
router.get("/api/node/:token", NodeResource.get);
|
||||
|
||||
router.put('/api/monitoring/confirm/:token', MonitoringResource.confirm);
|
||||
router.put('/api/monitoring/disable/:token', MonitoringResource.disable);
|
||||
router.put("/api/monitoring/confirm/:token", MonitoringResource.confirm);
|
||||
router.put("/api/monitoring/disable/:token", MonitoringResource.disable);
|
||||
|
||||
router.get('/internal/api/statistics', StatisticsResource.get);
|
||||
router.get("/internal/api/statistics", StatisticsResource.get);
|
||||
|
||||
router.get('/internal/api/tasks', TaskResource.getAll);
|
||||
router.put('/internal/api/tasks/run/:id', TaskResource.run);
|
||||
router.put('/internal/api/tasks/enable/:id', TaskResource.enable);
|
||||
router.put('/internal/api/tasks/disable/:id', TaskResource.disable);
|
||||
router.get("/internal/api/tasks", TaskResource.getAll);
|
||||
router.put("/internal/api/tasks/run/:id", TaskResource.run);
|
||||
router.put("/internal/api/tasks/enable/:id", TaskResource.enable);
|
||||
router.put("/internal/api/tasks/disable/:id", TaskResource.disable);
|
||||
|
||||
router.get('/internal/api/monitoring', MonitoringResource.getAll);
|
||||
router.get("/internal/api/monitoring", MonitoringResource.getAll);
|
||||
|
||||
router.get('/internal/api/mails', MailResource.getAll);
|
||||
router.get('/internal/api/mails/:id', MailResource.get);
|
||||
router.delete('/internal/api/mails/:id', MailResource.remove);
|
||||
router.put('/internal/api/mails/reset/:id', MailResource.resetFailures);
|
||||
router.get("/internal/api/mails", MailResource.getAll);
|
||||
router.get("/internal/api/mails/:id", MailResource.get);
|
||||
router.delete("/internal/api/mails/:id", MailResource.remove);
|
||||
router.put("/internal/api/mails/reset/:id", MailResource.resetFailures);
|
||||
|
||||
router.put('/internal/api/nodes/:token', NodeResource.update);
|
||||
router.delete('/internal/api/nodes/:token', NodeResource.remove);
|
||||
router.get('/internal/api/nodes', NodeResource.getAll);
|
||||
router.get('/internal/api/nodes/:token', NodeResource.get);
|
||||
router.put("/internal/api/nodes/:token", NodeResource.update);
|
||||
router.delete("/internal/api/nodes/:token", NodeResource.remove);
|
||||
router.get("/internal/api/nodes", NodeResource.getAll);
|
||||
router.get("/internal/api/nodes/:token", NodeResource.get);
|
||||
|
||||
app.use(config.server.rootPath, router);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import _ from "lodash";
|
||||
import moment, {Moment} from "moment";
|
||||
import {db} from "../db/database";
|
||||
import moment, { Moment } from "moment";
|
||||
import { db } from "../db/database";
|
||||
import Logger from "../logger";
|
||||
import * as MailTemplateService from "./mailTemplateService";
|
||||
import * as Resources from "../utils/resources";
|
||||
import {RestParams} from "../utils/resources";
|
||||
import { RestParams } from "../utils/resources";
|
||||
import {
|
||||
EmailAddress,
|
||||
isJSONObject,
|
||||
|
@ -16,32 +16,31 @@ import {
|
|||
MailSortField,
|
||||
MailType,
|
||||
parseJSON,
|
||||
UnixTimestampSeconds
|
||||
UnixTimestampSeconds,
|
||||
} from "../types";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import {send} from "../mail";
|
||||
import { send } from "../mail";
|
||||
|
||||
type EmaiQueueRow = {
|
||||
id: MailId,
|
||||
created_at: UnixTimestampSeconds,
|
||||
data: string,
|
||||
email: string,
|
||||
failures: number,
|
||||
modified_at: UnixTimestampSeconds,
|
||||
recipient: EmailAddress,
|
||||
sender: EmailAddress,
|
||||
id: MailId;
|
||||
created_at: UnixTimestampSeconds;
|
||||
data: string;
|
||||
email: string;
|
||||
failures: number;
|
||||
modified_at: UnixTimestampSeconds;
|
||||
recipient: EmailAddress;
|
||||
sender: EmailAddress;
|
||||
};
|
||||
|
||||
const MAIL_QUEUE_DB_BATCH_SIZE = 50;
|
||||
|
||||
async function sendMail(options: Mail): Promise<void> {
|
||||
Logger
|
||||
.tag('mail', 'queue')
|
||||
.info(
|
||||
'Sending pending mail[%d] of type %s. ' +
|
||||
'Had %d failures before.',
|
||||
options.id, options.email, options.failures
|
||||
);
|
||||
Logger.tag("mail", "queue").info(
|
||||
"Sending pending mail[%d] of type %s. " + "Had %d failures before.",
|
||||
options.id,
|
||||
options.email,
|
||||
options.failures
|
||||
);
|
||||
|
||||
const renderedTemplate = await MailTemplateService.render(options);
|
||||
|
||||
|
@ -49,21 +48,24 @@ async function sendMail(options: Mail): Promise<void> {
|
|||
from: options.sender,
|
||||
to: options.recipient,
|
||||
subject: renderedTemplate.subject,
|
||||
html: renderedTemplate.body
|
||||
html: renderedTemplate.body,
|
||||
};
|
||||
|
||||
await send(mailOptions);
|
||||
|
||||
Logger.tag('mail', 'queue').info('Mail[%d] has been send.', options.id);
|
||||
Logger.tag("mail", "queue").info("Mail[%d] has been send.", options.id);
|
||||
}
|
||||
|
||||
async function findPendingMailsBefore(beforeMoment: Moment, limit: number): Promise<Mail[]> {
|
||||
async function findPendingMailsBefore(
|
||||
beforeMoment: Moment,
|
||||
limit: number
|
||||
): Promise<Mail[]> {
|
||||
const rows = await db.all<EmaiQueueRow>(
|
||||
'SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?',
|
||||
[beforeMoment.unix(), 5, limit],
|
||||
"SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?",
|
||||
[beforeMoment.unix(), 5, limit]
|
||||
);
|
||||
|
||||
return rows.map(row => {
|
||||
return rows.map((row) => {
|
||||
const mailType = row.email;
|
||||
if (!isMailType(mailType)) {
|
||||
throw new Error(`Invalid mailtype in database: ${mailType}`);
|
||||
|
@ -84,13 +86,15 @@ async function findPendingMailsBefore(beforeMoment: Moment, limit: number): Prom
|
|||
}
|
||||
|
||||
async function removePendingMailFromQueue(id: MailId): Promise<void> {
|
||||
await db.run('DELETE FROM email_queue WHERE id = ?', [id]);
|
||||
await db.run("DELETE FROM email_queue WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
async function incrementFailureCounterForPendingEmail(id: MailId): Promise<void> {
|
||||
async function incrementFailureCounterForPendingEmail(
|
||||
id: MailId
|
||||
): Promise<void> {
|
||||
await db.run(
|
||||
'UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?',
|
||||
[moment().unix(), id],
|
||||
"UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?",
|
||||
[moment().unix(), id]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -99,7 +103,10 @@ async function sendPendingMail(pendingMail: Mail): Promise<void> {
|
|||
await sendMail(pendingMail);
|
||||
} catch (error) {
|
||||
// we only log the error and increment the failure counter as we want to continue with pending mails
|
||||
Logger.tag('mail', 'queue').error('Error sending pending mail[' + pendingMail.id + ']:', error);
|
||||
Logger.tag("mail", "queue").error(
|
||||
"Error sending pending mail[" + pendingMail.id + "]:",
|
||||
error
|
||||
);
|
||||
|
||||
await incrementFailureCounterForPendingEmail(pendingMail.id);
|
||||
return;
|
||||
|
@ -109,22 +116,29 @@ async function sendPendingMail(pendingMail: Mail): Promise<void> {
|
|||
}
|
||||
|
||||
async function doGetMail(id: MailId): Promise<Mail> {
|
||||
const row = await db.get<Mail>('SELECT * FROM email_queue WHERE id = ?', [id]);
|
||||
const row = await db.get<Mail>("SELECT * FROM email_queue WHERE id = ?", [
|
||||
id,
|
||||
]);
|
||||
if (row === undefined) {
|
||||
throw {data: 'Mail not found.', type: ErrorTypes.notFound};
|
||||
throw { data: "Mail not found.", type: ErrorTypes.notFound };
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function enqueue(sender: string, recipient: string, email: MailType, data: MailData): Promise<void> {
|
||||
export async function enqueue(
|
||||
sender: string,
|
||||
recipient: string,
|
||||
email: MailType,
|
||||
data: MailData
|
||||
): Promise<void> {
|
||||
if (!_.isPlainObject(data)) {
|
||||
throw new Error('Unexpected data: ' + data);
|
||||
throw new Error("Unexpected data: " + data);
|
||||
}
|
||||
await db.run(
|
||||
'INSERT INTO email_queue ' +
|
||||
'(failures, sender, recipient, email, data) ' +
|
||||
'VALUES (?, ?, ?, ?, ?)',
|
||||
[0, sender, recipient, email, JSON.stringify(data)],
|
||||
"INSERT INTO email_queue " +
|
||||
"(failures, sender, recipient, email, data) " +
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
[0, sender, recipient, email, JSON.stringify(data)]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -132,10 +146,12 @@ export async function getMail(id: MailId): Promise<Mail> {
|
|||
return await doGetMail(id);
|
||||
}
|
||||
|
||||
export async function getPendingMails(restParams: RestParams): Promise<{ mails: Mail[], total: number }> {
|
||||
export async function getPendingMails(
|
||||
restParams: RestParams
|
||||
): Promise<{ mails: Mail[]; total: number }> {
|
||||
const row = await db.get<{ total: number }>(
|
||||
'SELECT count(*) AS total FROM email_queue',
|
||||
[],
|
||||
"SELECT count(*) AS total FROM email_queue",
|
||||
[]
|
||||
);
|
||||
|
||||
const total = row?.total || 0;
|
||||
|
@ -144,18 +160,18 @@ export async function getPendingMails(restParams: RestParams): Promise<{ mails:
|
|||
restParams,
|
||||
MailSortField.ID,
|
||||
isMailSortField,
|
||||
['id', 'failures', 'sender', 'recipient', 'email']
|
||||
["id", "failures", "sender", "recipient", "email"]
|
||||
);
|
||||
|
||||
const mails = await db.all(
|
||||
'SELECT * FROM email_queue WHERE ' + filter.query,
|
||||
filter.params,
|
||||
"SELECT * FROM email_queue WHERE " + filter.query,
|
||||
filter.params
|
||||
);
|
||||
|
||||
return {
|
||||
mails,
|
||||
total
|
||||
}
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteMail(id: MailId): Promise<void> {
|
||||
|
@ -164,34 +180,39 @@ export async function deleteMail(id: MailId): Promise<void> {
|
|||
|
||||
export async function resetFailures(id: MailId): Promise<Mail> {
|
||||
const statement = await db.run(
|
||||
'UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?',
|
||||
[moment().unix(), id],
|
||||
"UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?",
|
||||
[moment().unix(), id]
|
||||
);
|
||||
|
||||
if (!statement.changes) {
|
||||
throw new Error('Error: could not reset failure count for mail: ' + id);
|
||||
throw new Error("Error: could not reset failure count for mail: " + id);
|
||||
}
|
||||
|
||||
return await doGetMail(id);
|
||||
}
|
||||
|
||||
export async function sendPendingMails(): Promise<void> {
|
||||
Logger.tag('mail', 'queue').debug('Start sending pending mails...');
|
||||
Logger.tag("mail", "queue").debug("Start sending pending mails...");
|
||||
|
||||
const startTime = moment();
|
||||
|
||||
while (true) {
|
||||
Logger.tag('mail', 'queue').debug('Sending next batch...');
|
||||
let pendingMails = await findPendingMailsBefore(
|
||||
startTime,
|
||||
MAIL_QUEUE_DB_BATCH_SIZE
|
||||
);
|
||||
|
||||
const pendingMails = await findPendingMailsBefore(startTime, MAIL_QUEUE_DB_BATCH_SIZE);
|
||||
|
||||
if (_.isEmpty(pendingMails)) {
|
||||
Logger.tag('mail', 'queue').debug('Done sending pending mails.');
|
||||
return;
|
||||
}
|
||||
while (!_.isEmpty(pendingMails)) {
|
||||
Logger.tag("mail", "queue").debug("Sending next batch...");
|
||||
|
||||
for (const pendingMail of pendingMails) {
|
||||
await sendPendingMail(pendingMail);
|
||||
}
|
||||
|
||||
pendingMails = await findPendingMailsBefore(
|
||||
startTime,
|
||||
MAIL_QUEUE_DB_BATCH_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
Logger.tag("mail", "queue").debug("Done sending pending mails.");
|
||||
}
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
import _ from "lodash";
|
||||
import deepExtend from "deep-extend";
|
||||
import {readFileSync, promises as fs} from "graceful-fs";
|
||||
import { readFileSync, promises as fs } from "graceful-fs";
|
||||
import moment from "moment";
|
||||
import {htmlToText} from "nodemailer-html-to-text";
|
||||
import { htmlToText } from "nodemailer-html-to-text";
|
||||
|
||||
import {config} from "../config";
|
||||
import { config } from "../config";
|
||||
import Logger from "../logger";
|
||||
import {editNodeUrl} from "../utils/urlBuilder";
|
||||
import {Transporter} from "nodemailer";
|
||||
import {MailData, Mail} from "../types";
|
||||
import { editNodeUrl } from "../utils/urlBuilder";
|
||||
import { Transporter } from "nodemailer";
|
||||
import { MailData, Mail } from "../types";
|
||||
|
||||
const templateBasePath = __dirname + '/../mailTemplates';
|
||||
const snippetsBasePath = templateBasePath + '/snippets';
|
||||
const templateBasePath = __dirname + "/../mailTemplates";
|
||||
const snippetsBasePath = templateBasePath + "/snippets";
|
||||
|
||||
const templateFunctions: {
|
||||
[key: string]:
|
||||
| ((name: string, data: MailData) => string)
|
||||
| ((data: MailData) => string)
|
||||
| ((href: string, text: string) => string)
|
||||
| ((unix: number) => string)
|
||||
| ((unix: number) => string);
|
||||
} = {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function renderSnippet(this: any, name: string, data: MailData): string {
|
||||
const snippetFile = snippetsBasePath + '/' + name + '.html';
|
||||
const snippetFile = snippetsBasePath + "/" + name + ".html";
|
||||
|
||||
return _.template(readFileSync(snippetFile).toString())(deepExtend(
|
||||
{},
|
||||
this, // parent data
|
||||
data,
|
||||
templateFunctions
|
||||
));
|
||||
return _.template(readFileSync(snippetFile).toString())(
|
||||
deepExtend(
|
||||
{},
|
||||
this, // parent data
|
||||
data,
|
||||
templateFunctions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function snippet(name: string): ((this: any, data: MailData) => string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function snippet(name: string): (this: any, data: MailData) => string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (this: any, data: MailData): string {
|
||||
return renderSnippet.bind(this)(name, data);
|
||||
};
|
||||
|
@ -44,7 +49,7 @@ function renderLink(href: string, text: string): string {
|
|||
'<a href="<%- href %>#" style="color: #E5287A;"><%- text %></a>'
|
||||
)({
|
||||
href: href,
|
||||
text: text || href
|
||||
text: text || href,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -53,17 +58,17 @@ function renderHR(): string {
|
|||
}
|
||||
|
||||
function formatDateTime(unix: number): string {
|
||||
return moment.unix(unix).locale('de').local().format('DD.MM.YYYY HH:mm');
|
||||
return moment.unix(unix).locale("de").local().format("DD.MM.YYYY HH:mm");
|
||||
}
|
||||
|
||||
function formatFromNow(unix: number): string {
|
||||
return moment.unix(unix).locale('de').fromNow();
|
||||
return moment.unix(unix).locale("de").fromNow();
|
||||
}
|
||||
|
||||
templateFunctions.header = snippet('header');
|
||||
templateFunctions.footer = snippet('footer');
|
||||
templateFunctions.header = snippet("header");
|
||||
templateFunctions.footer = snippet("footer");
|
||||
|
||||
templateFunctions.monitoringFooter = snippet('monitoring-footer');
|
||||
templateFunctions.monitoringFooter = snippet("monitoring-footer");
|
||||
|
||||
templateFunctions.snippet = renderSnippet;
|
||||
|
||||
|
@ -73,24 +78,29 @@ templateFunctions.hr = renderHR;
|
|||
templateFunctions.formatDateTime = formatDateTime;
|
||||
templateFunctions.formatFromNow = formatFromNow;
|
||||
|
||||
export function configureTransporter (transporter: Transporter): void {
|
||||
transporter.use('compile', htmlToText({
|
||||
tables: ['.table']
|
||||
}));
|
||||
export function configureTransporter(transporter: Transporter): void {
|
||||
transporter.use(
|
||||
"compile",
|
||||
htmlToText({
|
||||
tables: [".table"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function render(mailOptions: Mail): Promise<{subject: string, body: string}> {
|
||||
const templatePathPrefix = templateBasePath + '/' + mailOptions.email;
|
||||
export async function render(
|
||||
mailOptions: Mail
|
||||
): Promise<{ subject: string; body: string }> {
|
||||
const templatePathPrefix = templateBasePath + "/" + mailOptions.email;
|
||||
|
||||
const subject = await fs.readFile(templatePathPrefix + '.subject.txt');
|
||||
const body = await fs.readFile(templatePathPrefix + '.body.html');
|
||||
const subject = await fs.readFile(templatePathPrefix + ".subject.txt");
|
||||
const body = await fs.readFile(templatePathPrefix + ".body.html");
|
||||
|
||||
const data = deepExtend(
|
||||
{},
|
||||
mailOptions.data,
|
||||
{
|
||||
community: config.client.community,
|
||||
editNodeUrl: editNodeUrl()
|
||||
editNodeUrl: editNodeUrl(),
|
||||
},
|
||||
templateFunctions
|
||||
);
|
||||
|
@ -98,12 +108,13 @@ export async function render(mailOptions: Mail): Promise<{subject: string, body:
|
|||
try {
|
||||
return {
|
||||
subject: _.template(subject.toString())(data).trim(),
|
||||
body: _.template(body.toString())(data)
|
||||
body: _.template(body.toString())(data),
|
||||
};
|
||||
} catch (error) {
|
||||
Logger
|
||||
.tag('mail', 'template')
|
||||
.error('Error rendering template for mail[' + mailOptions.id + ']:', error);
|
||||
Logger.tag("mail", "template").error(
|
||||
"Error rendering template for mail[" + mailOptions.id + "]:",
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {ParsedNode, parseNode, parseNodesJson} from "./monitoringService";
|
||||
import {Domain, MAC, OnlineState, Site, UnixTimestampSeconds} from "../types";
|
||||
import Logger from '../logger';
|
||||
import {MockLogger} from "../__mocks__/logger";
|
||||
import {now, parseTimestamp} from "../utils/time";
|
||||
import { ParsedNode, parseNode, parseNodesJson } from "./monitoringService";
|
||||
import { Domain, MAC, OnlineState, Site, UnixTimestampSeconds } from "../types";
|
||||
import Logger from "../logger";
|
||||
import { MockLogger } from "../__mocks__/logger";
|
||||
import { now, parseTimestamp } from "../utils/time";
|
||||
|
||||
const mockedLogger = Logger as MockLogger;
|
||||
|
||||
jest.mock('../logger');
|
||||
jest.mock('../db/database');
|
||||
jest.mock("../logger");
|
||||
jest.mock("../db/database");
|
||||
|
||||
const NODES_JSON_INVALID_VERSION = 1;
|
||||
const NODES_JSON_VALID_VERSION = 2;
|
||||
|
@ -25,16 +25,7 @@ beforeEach(() => {
|
|||
mockedLogger.reset();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for undefined node data', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = undefined;
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for empty node data', () => {
|
||||
test("parseNode() should fail parsing node for empty node data", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {};
|
||||
|
@ -43,159 +34,159 @@ test('parseNode() should fail parsing node for empty node data', () => {
|
|||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for empty node info', () => {
|
||||
test("parseNode() should fail parsing node for empty node info", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {}
|
||||
nodeinfo: {},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for non-string node id', () => {
|
||||
test("parseNode() should fail parsing node for non-string node id", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: 42
|
||||
}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for empty node id', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: ""
|
||||
}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for empty network info', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {}
|
||||
}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for invalid mac', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "xxx"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for missing flags', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for empty flags', () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
}
|
||||
node_id: 42,
|
||||
},
|
||||
flags: {}
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for missing last seen timestamp', () => {
|
||||
test("parseNode() should fail parsing node for empty node id", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "",
|
||||
},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test("parseNode() should fail parsing node for empty network info", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {},
|
||||
},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test("parseNode() should fail parsing node for invalid mac", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
}
|
||||
mac: "xxx",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test("parseNode() should fail parsing node for missing flags", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test("parseNode() should fail parsing node for empty flags", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
},
|
||||
flags: {},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test("parseNode() should fail parsing node for missing last seen timestamp", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
}
|
||||
online: true,
|
||||
},
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should fail parsing node for invalid last seen timestamp', () => {
|
||||
test("parseNode() should fail parsing node for invalid last seen timestamp", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
}
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
online: true,
|
||||
},
|
||||
lastseen: 42
|
||||
lastseen: 42,
|
||||
};
|
||||
|
||||
// then
|
||||
expect(() => parseNode(importTimestamp, nodeData)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNode() should succeed parsing node without site and domain', () => {
|
||||
test("parseNode() should succeed parsing node without site and domain", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
}
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
online: true,
|
||||
},
|
||||
lastseen: TIMESTAMP_VALID_STRING
|
||||
lastseen: TIMESTAMP_VALID_STRING,
|
||||
};
|
||||
|
||||
// then
|
||||
|
@ -210,22 +201,22 @@ test('parseNode() should succeed parsing node without site and domain', () => {
|
|||
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
||||
});
|
||||
|
||||
test('parseNode() should succeed parsing node with site and domain', () => {
|
||||
test("parseNode() should succeed parsing node with site and domain", () => {
|
||||
// given
|
||||
const importTimestamp = now();
|
||||
const nodeData = {
|
||||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
system: {
|
||||
site_code: "test-site",
|
||||
domain_code: "test-domain"
|
||||
}
|
||||
domain_code: "test-domain",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
online: true,
|
||||
},
|
||||
lastseen: TIMESTAMP_VALID_STRING,
|
||||
};
|
||||
|
@ -242,7 +233,7 @@ test('parseNode() should succeed parsing node with site and domain', () => {
|
|||
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing empty string', () => {
|
||||
test("parseNodesJson() should fail parsing empty string", () => {
|
||||
// given
|
||||
const json = "";
|
||||
|
||||
|
@ -250,7 +241,7 @@ test('parseNodesJson() should fail parsing empty string', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing malformed JSON', () => {
|
||||
test("parseNodesJson() should fail parsing malformed JSON", () => {
|
||||
// given
|
||||
const json = '{"version": 2]';
|
||||
|
||||
|
@ -258,7 +249,7 @@ test('parseNodesJson() should fail parsing malformed JSON', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing JSON null', () => {
|
||||
test("parseNodesJson() should fail parsing JSON null", () => {
|
||||
// given
|
||||
const json = JSON.stringify(null);
|
||||
|
||||
|
@ -266,7 +257,7 @@ test('parseNodesJson() should fail parsing JSON null', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing JSON string', () => {
|
||||
test("parseNodesJson() should fail parsing JSON string", () => {
|
||||
// given
|
||||
const json = JSON.stringify("foo");
|
||||
|
||||
|
@ -274,7 +265,7 @@ test('parseNodesJson() should fail parsing JSON string', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing JSON number', () => {
|
||||
test("parseNodesJson() should fail parsing JSON number", () => {
|
||||
// given
|
||||
const json = JSON.stringify(42);
|
||||
|
||||
|
@ -282,7 +273,7 @@ test('parseNodesJson() should fail parsing JSON number', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing empty JSON object', () => {
|
||||
test("parseNodesJson() should fail parsing empty JSON object", () => {
|
||||
// given
|
||||
const json = JSON.stringify({});
|
||||
|
||||
|
@ -290,57 +281,57 @@ test('parseNodesJson() should fail parsing empty JSON object', () => {
|
|||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing for mismatching version', () => {
|
||||
test("parseNodesJson() should fail parsing for mismatching version", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_INVALID_VERSION
|
||||
version: NODES_JSON_INVALID_VERSION,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing for missing timestamp', () => {
|
||||
test("parseNodesJson() should fail parsing for missing timestamp", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
nodes: []
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
// then
|
||||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing for invalid timestamp', () => {
|
||||
test("parseNodesJson() should fail parsing for invalid timestamp", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
timestamp: TIMESTAMP_INVALID_STRING,
|
||||
nodes: []
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
// then
|
||||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should fail parsing for nodes object instead of array', () => {
|
||||
test("parseNodesJson() should fail parsing for nodes object instead of array", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
timestamp: TIMESTAMP_VALID_STRING,
|
||||
nodes: {}
|
||||
nodes: {},
|
||||
});
|
||||
|
||||
// then
|
||||
expect(() => parseNodesJson(json)).toThrowError();
|
||||
});
|
||||
|
||||
test('parseNodesJson() should succeed parsing no nodes', () => {
|
||||
test("parseNodesJson() should succeed parsing no nodes", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
timestamp: TIMESTAMP_VALID_STRING,
|
||||
nodes: []
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
// when
|
||||
|
@ -352,7 +343,7 @@ test('parseNodesJson() should succeed parsing no nodes', () => {
|
|||
expect(result.totalNodesCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('parseNodesJson() should skip parsing invalid nodes', () => {
|
||||
test("parseNodesJson() should skip parsing invalid nodes", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
|
@ -363,19 +354,19 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
|
|||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
system: {
|
||||
site_code: "test-site",
|
||||
domain_code: "test-domain"
|
||||
}
|
||||
domain_code: "test-domain",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
online: true,
|
||||
},
|
||||
lastseen: TIMESTAMP_INVALID_STRING,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// when
|
||||
|
@ -385,10 +376,13 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
|
|||
expect(result.nodes).toEqual([]);
|
||||
expect(result.failedNodesCount).toEqual(2);
|
||||
expect(result.totalNodesCount).toEqual(2);
|
||||
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2);
|
||||
expect(
|
||||
mockedLogger.getMessages("error", "monitoring", "parsing-nodes-json")
|
||||
.length
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
test('parseNodesJson() should parse valid nodes', () => {
|
||||
test("parseNodesJson() should parse valid nodes", () => {
|
||||
// given
|
||||
const json = JSON.stringify({
|
||||
version: NODES_JSON_VALID_VERSION,
|
||||
|
@ -399,19 +393,19 @@ test('parseNodesJson() should parse valid nodes', () => {
|
|||
nodeinfo: {
|
||||
node_id: "1234567890ab",
|
||||
network: {
|
||||
mac: "12:34:56:78:90:ab"
|
||||
mac: "12:34:56:78:90:ab",
|
||||
},
|
||||
system: {
|
||||
site_code: "test-site",
|
||||
domain_code: "test-domain"
|
||||
}
|
||||
domain_code: "test-domain",
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
online: true
|
||||
online: true,
|
||||
},
|
||||
lastseen: TIMESTAMP_VALID_STRING,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// when
|
||||
|
@ -430,5 +424,8 @@ test('parseNodesJson() should parse valid nodes', () => {
|
|||
expect(result.nodes).toEqual([expectedParsedNode]);
|
||||
expect(result.failedNodesCount).toEqual(1);
|
||||
expect(result.totalNodesCount).toEqual(2);
|
||||
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1);
|
||||
expect(
|
||||
mockedLogger.getMessages("error", "monitoring", "parsing-nodes-json")
|
||||
.length
|
||||
).toEqual(1);
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,21 +1,25 @@
|
|||
import async from "async";
|
||||
import crypto from "crypto";
|
||||
import oldFs, {promises as fs} from "graceful-fs";
|
||||
import oldFs, { promises as fs } from "graceful-fs";
|
||||
import glob from "glob";
|
||||
|
||||
import {config} from "../config";
|
||||
import { config } from "../config";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import Logger from "../logger";
|
||||
import logger from "../logger";
|
||||
import * as MailService from "../services/mailService";
|
||||
import {normalizeString} from "../shared/utils/strings";
|
||||
import {monitoringConfirmUrl, monitoringDisableUrl} from "../utils/urlBuilder";
|
||||
import { normalizeString } from "../shared/utils/strings";
|
||||
import {
|
||||
monitoringConfirmUrl,
|
||||
monitoringDisableUrl,
|
||||
} from "../utils/urlBuilder";
|
||||
import {
|
||||
BaseNode,
|
||||
Coordinates,
|
||||
CreateOrUpdateNode,
|
||||
EmailAddress,
|
||||
FastdKey,
|
||||
filterUndefinedFromJSON,
|
||||
Hostname,
|
||||
isFastdKey,
|
||||
isHostname,
|
||||
|
@ -36,27 +40,27 @@ import {
|
|||
TypeGuard,
|
||||
unhandledEnumField,
|
||||
UnixTimestampMilliseconds,
|
||||
UnixTimestampSeconds
|
||||
UnixTimestampSeconds,
|
||||
} from "../types";
|
||||
import util from "util";
|
||||
|
||||
const pglob = util.promisify(glob);
|
||||
|
||||
type NodeFilter = {
|
||||
hostname?: Hostname,
|
||||
mac?: MAC,
|
||||
key?: FastdKey,
|
||||
token?: Token,
|
||||
monitoringToken?: MonitoringToken,
|
||||
}
|
||||
hostname?: Hostname;
|
||||
mac?: MAC;
|
||||
key?: FastdKey;
|
||||
token?: Token;
|
||||
monitoringToken?: MonitoringToken;
|
||||
};
|
||||
|
||||
type NodeFilenameParsed = {
|
||||
hostname?: Hostname,
|
||||
mac?: MAC,
|
||||
key?: FastdKey,
|
||||
token?: Token,
|
||||
monitoringToken?: MonitoringToken,
|
||||
}
|
||||
hostname?: Hostname;
|
||||
mac?: MAC;
|
||||
key?: FastdKey;
|
||||
token?: Token;
|
||||
monitoringToken?: MonitoringToken;
|
||||
};
|
||||
|
||||
enum LINE_PREFIX {
|
||||
HOSTNAME = "# Knotenname: ",
|
||||
|
@ -69,9 +73,10 @@ enum LINE_PREFIX {
|
|||
MONITORING_TOKEN = "# Monitoring-Token: ",
|
||||
}
|
||||
|
||||
|
||||
function generateToken<Type extends string & { readonly __tag: symbol } = never>(): Type {
|
||||
return crypto.randomBytes(8).toString('hex') as Type;
|
||||
function generateToken<
|
||||
Type extends string & { readonly __tag: symbol } = never
|
||||
>(): Type {
|
||||
return crypto.randomBytes(8).toString("hex") as Type;
|
||||
}
|
||||
|
||||
function toNodeFilesPattern(filter: NodeFilter): string {
|
||||
|
@ -83,9 +88,9 @@ function toNodeFilesPattern(filter: NodeFilter): string {
|
|||
filter.monitoringToken,
|
||||
];
|
||||
|
||||
const pattern = fields.map((value) => value || '*').join('@');
|
||||
const pattern = fields.map((value) => value || "*").join("@");
|
||||
|
||||
return config.server.peersPath + '/' + pattern.toLowerCase();
|
||||
return config.server.peersPath + "/" + pattern.toLowerCase();
|
||||
}
|
||||
|
||||
function findNodeFiles(filter: NodeFilter): Promise<string[]> {
|
||||
|
@ -97,24 +102,25 @@ function findNodeFilesSync(filter: NodeFilter) {
|
|||
}
|
||||
|
||||
async function findFilesInPeersPath(): Promise<string[]> {
|
||||
const files = await pglob(config.server.peersPath + '/*');
|
||||
const files = await pglob(config.server.peersPath + "/*");
|
||||
|
||||
return await async.filter(files, (file, fileCallback) => {
|
||||
if (file[0] === '.') {
|
||||
if (file[0] === ".") {
|
||||
return fileCallback(null, false);
|
||||
}
|
||||
|
||||
fs.lstat(file)
|
||||
.then(stats => fileCallback(null, stats.isFile()))
|
||||
.then((stats) => fileCallback(null, stats.isFile()))
|
||||
.catch(fileCallback);
|
||||
});
|
||||
}
|
||||
|
||||
function parseNodeFilename(filename: string): NodeFilenameParsed {
|
||||
const parts = filename.split('@', 5);
|
||||
const parts = filename.split("@", 5);
|
||||
|
||||
function get<T>(isT: TypeGuard<T>, index: number): T | undefined {
|
||||
const value = index >= 0 && index < parts.length ? parts[index] : undefined;
|
||||
const value =
|
||||
index >= 0 && index < parts.length ? parts[index] : undefined;
|
||||
return isT(value) ? value : undefined;
|
||||
}
|
||||
|
||||
|
@ -140,35 +146,65 @@ function isDuplicate(filter: NodeFilter, token?: Token): boolean {
|
|||
return parseNodeFilename(files[0]).token !== token;
|
||||
}
|
||||
|
||||
function checkNoDuplicates(token: Token | undefined, node: BaseNode, nodeSecrets: NodeSecrets): void {
|
||||
if (isDuplicate({hostname: node.hostname}, token)) {
|
||||
throw {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict};
|
||||
function checkNoDuplicates(
|
||||
token: Token | undefined,
|
||||
node: BaseNode,
|
||||
nodeSecrets: NodeSecrets
|
||||
): void {
|
||||
if (isDuplicate({ hostname: node.hostname }, token)) {
|
||||
throw {
|
||||
data: { msg: "Already exists.", field: "hostname" },
|
||||
type: ErrorTypes.conflict,
|
||||
};
|
||||
}
|
||||
|
||||
if (node.key) {
|
||||
if (isDuplicate({key: node.key}, token)) {
|
||||
throw {data: {msg: 'Already exists.', field: 'key'}, type: ErrorTypes.conflict};
|
||||
if (isDuplicate({ key: node.key }, token)) {
|
||||
throw {
|
||||
data: { msg: "Already exists.", field: "key" },
|
||||
type: ErrorTypes.conflict,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isDuplicate({mac: node.mac}, token)) {
|
||||
throw {data: {msg: 'Already exists.', field: 'mac'}, type: ErrorTypes.conflict};
|
||||
if (isDuplicate({ mac: node.mac }, token)) {
|
||||
throw {
|
||||
data: { msg: "Already exists.", field: "mac" },
|
||||
type: ErrorTypes.conflict,
|
||||
};
|
||||
}
|
||||
|
||||
if (nodeSecrets.monitoringToken && isDuplicate({monitoringToken: nodeSecrets.monitoringToken}, token)) {
|
||||
throw {data: {msg: 'Already exists.', field: 'monitoringToken'}, type: ErrorTypes.conflict};
|
||||
if (
|
||||
nodeSecrets.monitoringToken &&
|
||||
isDuplicate({ monitoringToken: nodeSecrets.monitoringToken }, token)
|
||||
) {
|
||||
throw {
|
||||
data: { msg: "Already exists.", field: "monitoringToken" },
|
||||
type: ErrorTypes.conflict,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function toNodeFilename(token: Token, node: BaseNode, nodeSecrets: NodeSecrets): string {
|
||||
return config.server.peersPath + '/' +
|
||||
function toNodeFilename(
|
||||
token: Token,
|
||||
node: BaseNode,
|
||||
nodeSecrets: NodeSecrets
|
||||
): string {
|
||||
return (
|
||||
config.server.peersPath +
|
||||
"/" +
|
||||
(
|
||||
(node.hostname || '') + '@' +
|
||||
(node.mac || '') + '@' +
|
||||
(node.key || '') + '@' +
|
||||
(token || '') + '@' +
|
||||
(nodeSecrets.monitoringToken || '')
|
||||
).toLowerCase();
|
||||
(node.hostname || "") +
|
||||
"@" +
|
||||
(node.mac || "") +
|
||||
"@" +
|
||||
(node.key || "") +
|
||||
"@" +
|
||||
(token || "") +
|
||||
"@" +
|
||||
(nodeSecrets.monitoringToken || "")
|
||||
).toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeValue(
|
||||
|
@ -194,7 +230,10 @@ function getNodeValue(
|
|||
case LINE_PREFIX.MONITORING:
|
||||
if (node.monitoring && monitoringState === MonitoringState.ACTIVE) {
|
||||
return "aktiv";
|
||||
} else if (node.monitoring && monitoringState === MonitoringState.PENDING) {
|
||||
} else if (
|
||||
node.monitoring &&
|
||||
monitoringState === MonitoringState.PENDING
|
||||
) {
|
||||
return "pending";
|
||||
}
|
||||
return "";
|
||||
|
@ -210,13 +249,19 @@ async function writeNodeFile(
|
|||
token: Token,
|
||||
node: CreateOrUpdateNode,
|
||||
monitoringState: MonitoringState,
|
||||
nodeSecrets: NodeSecrets,
|
||||
nodeSecrets: NodeSecrets
|
||||
): Promise<StoredNode> {
|
||||
const filename = toNodeFilename(token, node, nodeSecrets);
|
||||
let data = '';
|
||||
let data = "";
|
||||
|
||||
for (const prefix of Object.values(LINE_PREFIX)) {
|
||||
data += `${prefix}${getNodeValue(prefix, token, node, monitoringState, nodeSecrets)}\n`;
|
||||
data += `${prefix}${getNodeValue(
|
||||
prefix,
|
||||
token,
|
||||
node,
|
||||
monitoringState,
|
||||
nodeSecrets
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
if (node.key) {
|
||||
|
@ -225,9 +270,9 @@ async function writeNodeFile(
|
|||
|
||||
// since node.js is single threaded we don't need a lock when working with synchronous operations
|
||||
if (isUpdate) {
|
||||
const files = findNodeFilesSync({token: token});
|
||||
const files = findNodeFilesSync({ token: token });
|
||||
if (files.length !== 1) {
|
||||
throw {data: 'Node not found.', type: ErrorTypes.notFound};
|
||||
throw { data: "Node not found.", type: ErrorTypes.notFound };
|
||||
}
|
||||
|
||||
checkNoDuplicates(token, node, nodeSecrets);
|
||||
|
@ -236,41 +281,65 @@ async function writeNodeFile(
|
|||
try {
|
||||
oldFs.unlinkSync(file);
|
||||
} catch (error) {
|
||||
Logger.tag('node', 'save').error('Could not delete old node file: ' + file, error);
|
||||
throw {data: 'Could not remove old node data.', type: ErrorTypes.internalError};
|
||||
Logger.tag("node", "save").error(
|
||||
"Could not delete old node file: " + file,
|
||||
error
|
||||
);
|
||||
throw {
|
||||
data: "Could not remove old node data.",
|
||||
type: ErrorTypes.internalError,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
checkNoDuplicates(undefined, node, nodeSecrets);
|
||||
}
|
||||
|
||||
try {
|
||||
oldFs.writeFileSync(filename, data, 'utf8');
|
||||
const {node: storedNode} = await parseNodeFile(filename);
|
||||
oldFs.writeFileSync(filename, data, "utf8");
|
||||
const { node: storedNode } = await parseNodeFile(filename);
|
||||
return storedNode;
|
||||
} catch (error) {
|
||||
Logger.tag('node', 'save').error('Could not write node file: ' + filename, error);
|
||||
throw {data: 'Could not write node data.', type: ErrorTypes.internalError};
|
||||
Logger.tag("node", "save").error(
|
||||
"Could not write node file: " + filename,
|
||||
error
|
||||
);
|
||||
throw {
|
||||
data: "Could not write node data.",
|
||||
type: ErrorTypes.internalError,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNodeFile(token: Token): Promise<void> {
|
||||
let files;
|
||||
try {
|
||||
files = await findNodeFiles({token: token});
|
||||
files = await findNodeFiles({ token: token });
|
||||
} catch (error) {
|
||||
Logger.tag('node', 'delete').error('Could not find node file: ' + files, error);
|
||||
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
||||
Logger.tag("node", "delete").error(
|
||||
"Could not find node file: " + files,
|
||||
error
|
||||
);
|
||||
throw {
|
||||
data: "Could not delete node.",
|
||||
type: ErrorTypes.internalError,
|
||||
};
|
||||
}
|
||||
|
||||
if (files.length !== 1) {
|
||||
throw {data: 'Node not found.', type: ErrorTypes.notFound};
|
||||
throw { data: "Node not found.", type: ErrorTypes.notFound };
|
||||
}
|
||||
|
||||
try {
|
||||
oldFs.unlinkSync(files[0]);
|
||||
} catch (error) {
|
||||
Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error);
|
||||
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
||||
Logger.tag("node", "delete").error(
|
||||
"Could not delete node file: " + files,
|
||||
error
|
||||
);
|
||||
throw {
|
||||
data: "Could not delete node.",
|
||||
type: ErrorTypes.internalError,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,10 +353,7 @@ class StoredNodeBuilder {
|
|||
public mac: MAC = "" as MAC; // FIXME: Either make mac optional in Node or handle this!
|
||||
public monitoringState: MonitoringState = MonitoringState.DISABLED;
|
||||
|
||||
constructor(
|
||||
public readonly modifiedAt: UnixTimestampSeconds,
|
||||
) {
|
||||
}
|
||||
constructor(public readonly modifiedAt: UnixTimestampSeconds) {}
|
||||
|
||||
public build(): StoredNode {
|
||||
const node = {
|
||||
|
@ -304,14 +370,22 @@ class StoredNodeBuilder {
|
|||
|
||||
if (!isStoredNode(node)) {
|
||||
logger.tag("NodeService").error("Not a valid StoredNode:", node);
|
||||
throw {data: "Could not build StoredNode.", type: ErrorTypes.internalError};
|
||||
throw {
|
||||
data: "Could not build StoredNode.",
|
||||
type: ErrorTypes.internalError,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeValue(prefix: LINE_PREFIX, node: StoredNodeBuilder, nodeSecrets: NodeSecrets, value: string) {
|
||||
function setNodeValue(
|
||||
prefix: LINE_PREFIX,
|
||||
node: StoredNodeBuilder,
|
||||
nodeSecrets: NodeSecrets,
|
||||
value: string
|
||||
) {
|
||||
switch (prefix) {
|
||||
case LINE_PREFIX.HOSTNAME:
|
||||
node.hostname = value as Hostname;
|
||||
|
@ -331,12 +405,16 @@ function setNodeValue(prefix: LINE_PREFIX, node: StoredNodeBuilder, nodeSecrets:
|
|||
case LINE_PREFIX.TOKEN:
|
||||
node.token = value as Token;
|
||||
break;
|
||||
case LINE_PREFIX.MONITORING:
|
||||
const active = value === 'aktiv';
|
||||
const pending = value === 'pending';
|
||||
node.monitoringState =
|
||||
active ? MonitoringState.ACTIVE : (pending ? MonitoringState.PENDING : MonitoringState.DISABLED);
|
||||
case LINE_PREFIX.MONITORING: {
|
||||
const active = value === "aktiv";
|
||||
const pending = value === "pending";
|
||||
node.monitoringState = active
|
||||
? MonitoringState.ACTIVE
|
||||
: pending
|
||||
? MonitoringState.PENDING
|
||||
: MonitoringState.DISABLED;
|
||||
break;
|
||||
}
|
||||
case LINE_PREFIX.MONITORING_TOKEN:
|
||||
nodeSecrets.monitoringToken = value as MonitoringToken;
|
||||
break;
|
||||
|
@ -346,11 +424,14 @@ function setNodeValue(prefix: LINE_PREFIX, node: StoredNodeBuilder, nodeSecrets:
|
|||
}
|
||||
|
||||
async function getModifiedAt(file: string): Promise<UnixTimestampSeconds> {
|
||||
const modifiedAtMs = (await fs.lstat(file)).mtimeMs as UnixTimestampMilliseconds;
|
||||
const modifiedAtMs = (await fs.lstat(file))
|
||||
.mtimeMs as UnixTimestampMilliseconds;
|
||||
return toUnixTimestampSeconds(modifiedAtMs);
|
||||
}
|
||||
|
||||
async function parseNodeFile(file: string): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets }> {
|
||||
async function parseNodeFile(
|
||||
file: string
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets }> {
|
||||
const contents = await fs.readFile(file);
|
||||
const modifiedAt = await getModifiedAt(file);
|
||||
|
||||
|
@ -365,7 +446,9 @@ async function parseNodeFile(file: string): Promise<{ node: StoredNode, nodeSecr
|
|||
} else {
|
||||
for (const prefix of Object.values(LINE_PREFIX)) {
|
||||
if (line.substring(0, prefix.length) === prefix) {
|
||||
const value = normalizeString(line.substring(prefix.length));
|
||||
const value = normalizeString(
|
||||
line.substring(prefix.length)
|
||||
);
|
||||
setNodeValue(prefix, node, nodeSecrets, value);
|
||||
break;
|
||||
}
|
||||
|
@ -379,7 +462,9 @@ async function parseNodeFile(file: string): Promise<{ node: StoredNode, nodeSecr
|
|||
};
|
||||
}
|
||||
|
||||
async function findNodeDataByFilePattern(filter: NodeFilter): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets } | null> {
|
||||
async function findNodeDataByFilePattern(
|
||||
filter: NodeFilter
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets } | null> {
|
||||
const files = await findNodeFiles(filter);
|
||||
|
||||
if (files.length !== 1) {
|
||||
|
@ -390,22 +475,27 @@ async function findNodeDataByFilePattern(filter: NodeFilter): Promise<{ node: St
|
|||
return await parseNodeFile(file);
|
||||
}
|
||||
|
||||
async function getNodeDataByFilePattern(filter: NodeFilter): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets }> {
|
||||
async function getNodeDataByFilePattern(
|
||||
filter: NodeFilter
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets }> {
|
||||
const result = await findNodeDataByFilePattern(filter);
|
||||
if (!result) {
|
||||
throw {data: 'Node not found.', type: ErrorTypes.notFound};
|
||||
throw { data: "Node not found.", type: ErrorTypes.notFound };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function sendMonitoringConfirmationMail(node: StoredNode, nodeSecrets: NodeSecrets): Promise<void> {
|
||||
async function sendMonitoringConfirmationMail(
|
||||
node: StoredNode,
|
||||
nodeSecrets: NodeSecrets
|
||||
): Promise<void> {
|
||||
const monitoringToken = nodeSecrets.monitoringToken;
|
||||
if (!monitoringToken) {
|
||||
Logger
|
||||
.tag('monitoring', 'confirmation')
|
||||
.error('Could not enqueue confirmation mail. No monitoring token found.');
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
Logger.tag("monitoring", "confirmation").error(
|
||||
"Could not enqueue confirmation mail. No monitoring token found."
|
||||
);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
|
||||
const confirmUrl = monitoringConfirmUrl(monitoringToken);
|
||||
|
@ -413,26 +503,36 @@ async function sendMonitoringConfirmationMail(node: StoredNode, nodeSecrets: Nod
|
|||
|
||||
await MailService.enqueue(
|
||||
config.server.email.from,
|
||||
node.nickname + ' <' + node.email + '>',
|
||||
node.nickname + " <" + node.email + ">",
|
||||
MailType.MONITORING_CONFIRMATION,
|
||||
{
|
||||
node: node,
|
||||
node: filterUndefinedFromJSON(node),
|
||||
confirmUrl: confirmUrl,
|
||||
disableUrl: disableUrl
|
||||
},
|
||||
disableUrl: disableUrl,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function createNode(node: CreateOrUpdateNode): Promise<StoredNode> {
|
||||
export async function createNode(
|
||||
node: CreateOrUpdateNode
|
||||
): Promise<StoredNode> {
|
||||
const token: Token = generateToken();
|
||||
const nodeSecrets: NodeSecrets = {};
|
||||
|
||||
const monitoringState = node.monitoring ? MonitoringState.PENDING : MonitoringState.DISABLED;
|
||||
const monitoringState = node.monitoring
|
||||
? MonitoringState.PENDING
|
||||
: MonitoringState.DISABLED;
|
||||
if (node.monitoring) {
|
||||
nodeSecrets.monitoringToken = generateToken<MonitoringToken>();
|
||||
}
|
||||
|
||||
const createdNode = await writeNodeFile(false, token, node, monitoringState, nodeSecrets);
|
||||
const createdNode = await writeNodeFile(
|
||||
false,
|
||||
token,
|
||||
node,
|
||||
monitoringState,
|
||||
nodeSecrets
|
||||
);
|
||||
|
||||
if (createdNode.monitoringState == MonitoringState.PENDING) {
|
||||
await sendMonitoringConfirmationMail(createdNode, nodeSecrets);
|
||||
|
@ -441,8 +541,12 @@ export async function createNode(node: CreateOrUpdateNode): Promise<StoredNode>
|
|||
return createdNode;
|
||||
}
|
||||
|
||||
export async function updateNode(token: Token, node: CreateOrUpdateNode): Promise<StoredNode> {
|
||||
const {node: currentNode, nodeSecrets} = await getNodeDataWithSecretsByToken(token);
|
||||
export async function updateNode(
|
||||
token: Token,
|
||||
node: CreateOrUpdateNode
|
||||
): Promise<StoredNode> {
|
||||
const { node: currentNode, nodeSecrets } =
|
||||
await getNodeDataWithSecretsByToken(token);
|
||||
|
||||
let monitoringState = MonitoringState.DISABLED;
|
||||
let monitoringToken: MonitoringToken | undefined = undefined;
|
||||
|
@ -461,11 +565,12 @@ export async function updateNode(token: Token, node: CreateOrUpdateNode): Promis
|
|||
// new email so we need a new token and a reconfirmation
|
||||
monitoringState = MonitoringState.PENDING;
|
||||
monitoringToken = generateToken<MonitoringToken>();
|
||||
|
||||
} else {
|
||||
// email unchanged, keep token (fix if not set) and confirmation state
|
||||
monitoringState = currentNode.monitoringState;
|
||||
monitoringToken = nodeSecrets.monitoringToken || generateToken<MonitoringToken>();
|
||||
monitoringToken =
|
||||
nodeSecrets.monitoringToken ||
|
||||
generateToken<MonitoringToken>();
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -476,9 +581,15 @@ export async function updateNode(token: Token, node: CreateOrUpdateNode): Promis
|
|||
|
||||
nodeSecrets.monitoringToken = monitoringToken;
|
||||
|
||||
const storedNode = await writeNodeFile(true, token, node, monitoringState, nodeSecrets);
|
||||
const storedNode = await writeNodeFile(
|
||||
true,
|
||||
token,
|
||||
node,
|
||||
monitoringState,
|
||||
nodeSecrets
|
||||
);
|
||||
if (storedNode.monitoringState === MonitoringState.PENDING) {
|
||||
await sendMonitoringConfirmationMail(storedNode, nodeSecrets)
|
||||
await sendMonitoringConfirmationMail(storedNode, nodeSecrets);
|
||||
}
|
||||
|
||||
return storedNode;
|
||||
|
@ -488,7 +599,7 @@ export async function internalUpdateNode(
|
|||
token: Token,
|
||||
node: CreateOrUpdateNode,
|
||||
monitoringState: MonitoringState,
|
||||
nodeSecrets: NodeSecrets,
|
||||
nodeSecrets: NodeSecrets
|
||||
): Promise<StoredNode> {
|
||||
return await writeNodeFile(true, token, node, monitoringState, nodeSecrets);
|
||||
}
|
||||
|
@ -502,52 +613,58 @@ export async function getAllNodes(): Promise<StoredNode[]> {
|
|||
try {
|
||||
files = await findNodeFiles({});
|
||||
} catch (error) {
|
||||
Logger.tag('nodes').error('Error getting all nodes:', error);
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
Logger.tag("nodes").error("Error getting all nodes:", error);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
|
||||
const nodes: StoredNode[] = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const {node} = await parseNodeFile(file);
|
||||
const { node } = await parseNodeFile(file);
|
||||
nodes.push(node);
|
||||
} catch (error) {
|
||||
Logger.tag('nodes').error('Error getting all nodes:', error);
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
Logger.tag("nodes").error("Error getting all nodes:", error);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export async function findNodeDataWithSecretsByMac(mac: MAC): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets } | null> {
|
||||
return await findNodeDataByFilePattern({mac});
|
||||
export async function findNodeDataWithSecretsByMac(
|
||||
mac: MAC
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets } | null> {
|
||||
return await findNodeDataByFilePattern({ mac });
|
||||
}
|
||||
|
||||
export async function findNodeDataByMac(mac: MAC): Promise<StoredNode | null> {
|
||||
const result = await findNodeDataByFilePattern({mac});
|
||||
const result = await findNodeDataByFilePattern({ mac });
|
||||
return result ? result.node : null;
|
||||
}
|
||||
|
||||
export async function getNodeDataWithSecretsByToken(token: Token): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets }> {
|
||||
return await getNodeDataByFilePattern({token: token});
|
||||
export async function getNodeDataWithSecretsByToken(
|
||||
token: Token
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets }> {
|
||||
return await getNodeDataByFilePattern({ token: token });
|
||||
}
|
||||
|
||||
export async function getNodeDataByToken(token: Token): Promise<StoredNode> {
|
||||
const {node} = await getNodeDataByFilePattern({token: token});
|
||||
const { node } = await getNodeDataByFilePattern({ token: token });
|
||||
return node;
|
||||
}
|
||||
|
||||
export async function getNodeDataWithSecretsByMonitoringToken(
|
||||
monitoringToken: MonitoringToken
|
||||
): Promise<{ node: StoredNode, nodeSecrets: NodeSecrets }> {
|
||||
return await getNodeDataByFilePattern({monitoringToken: monitoringToken});
|
||||
): Promise<{ node: StoredNode; nodeSecrets: NodeSecrets }> {
|
||||
return await getNodeDataByFilePattern({ monitoringToken: monitoringToken });
|
||||
}
|
||||
|
||||
export async function getNodeDataByMonitoringToken(
|
||||
monitoringToken: MonitoringToken
|
||||
): Promise<StoredNode> {
|
||||
const {node} = await getNodeDataByFilePattern({monitoringToken: monitoringToken});
|
||||
const { node } = await getNodeDataByFilePattern({
|
||||
monitoringToken: monitoringToken,
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
|
@ -555,7 +672,7 @@ export async function fixNodeFilenames(): Promise<void> {
|
|||
const files = await findFilesInPeersPath();
|
||||
|
||||
for (const file of files) {
|
||||
const {node, nodeSecrets} = await parseNodeFile(file);
|
||||
const { node, nodeSecrets } = await parseNodeFile(file);
|
||||
|
||||
const expectedFilename = toNodeFilename(node.token, node, nodeSecrets);
|
||||
if (file !== expectedFilename) {
|
||||
|
@ -563,16 +680,23 @@ export async function fixNodeFilenames(): Promise<void> {
|
|||
await fs.rename(file, expectedFilename);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + error
|
||||
"Cannot rename file " +
|
||||
file +
|
||||
" to " +
|
||||
expectedFilename +
|
||||
" => " +
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function findNodesModifiedBefore(timestamp: UnixTimestampSeconds): Promise<StoredNode[]> {
|
||||
export async function findNodesModifiedBefore(
|
||||
timestamp: UnixTimestampSeconds
|
||||
): Promise<StoredNode[]> {
|
||||
const nodes = await getAllNodes();
|
||||
return nodes.filter(node => node.modifiedAt < timestamp);
|
||||
return nodes.filter((node) => node.modifiedAt < timestamp);
|
||||
}
|
||||
|
||||
export async function getNodeStatistics(): Promise<NodeStatistics> {
|
||||
|
@ -584,8 +708,8 @@ export async function getNodeStatistics(): Promise<NodeStatistics> {
|
|||
withCoords: 0,
|
||||
monitoring: {
|
||||
active: 0,
|
||||
pending: 0
|
||||
}
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
|
||||
for (const node of nodes) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ArrayField, Field, RawJsonField} from "sparkson";
|
||||
import { ArrayField, Field, RawJsonField } from "sparkson";
|
||||
|
||||
// Types shared with the client.
|
||||
export type TypeGuard<T> = (arg: unknown) => arg is T;
|
||||
|
@ -11,6 +11,19 @@ export function parseJSON(str: string): JSONValue {
|
|||
return json;
|
||||
}
|
||||
|
||||
export function filterUndefinedFromJSON(obj: {
|
||||
[key: string]: JSONValue | undefined;
|
||||
}): JSONObject {
|
||||
const result: JSONObject = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type JSONValue =
|
||||
| null
|
||||
| string
|
||||
|
@ -49,8 +62,7 @@ export function isJSONObject(arg: unknown): arg is JSONObject {
|
|||
return true;
|
||||
}
|
||||
|
||||
export interface JSONArray extends Array<JSONValue> {
|
||||
}
|
||||
export type JSONArray = Array<JSONValue>;
|
||||
|
||||
export const isJSONArray = toIsArray(isJSONValue);
|
||||
|
||||
|
@ -65,39 +77,59 @@ export function isObject(arg: unknown): arg is object {
|
|||
return arg !== null && typeof arg === "object";
|
||||
}
|
||||
|
||||
export function isPlainObject(arg: unknown): arg is { [key: string]: unknown } {
|
||||
return isObject(arg) && !Array.isArray(arg);
|
||||
}
|
||||
|
||||
export function hasOwnProperty<Key extends PropertyKey>(
|
||||
arg: unknown,
|
||||
key: Key
|
||||
): arg is Record<Key, unknown> {
|
||||
return isObject(arg) && key in arg;
|
||||
}
|
||||
|
||||
export function getFieldIfExists(
|
||||
arg: unknown,
|
||||
key: PropertyKey
|
||||
): unknown | undefined {
|
||||
return hasOwnProperty(arg, key) ? arg[key] : undefined;
|
||||
}
|
||||
|
||||
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
||||
if (!Array.isArray(arg)) {
|
||||
return false;
|
||||
}
|
||||
for (const element of arg) {
|
||||
if (!isT(element)) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isMap(arg: unknown): arg is Map<any, any> {
|
||||
export function isMap(arg: unknown): arg is Map<unknown, unknown> {
|
||||
return arg instanceof Map;
|
||||
}
|
||||
|
||||
export function isString(arg: unknown): arg is string {
|
||||
return typeof arg === "string"
|
||||
return typeof arg === "string";
|
||||
}
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
export function toIsNewtype<
|
||||
Type extends Value & { readonly __tag: symbol },
|
||||
Value,
|
||||
>(isValue: TypeGuard<Value>, _example: Type): TypeGuard<Type> {
|
||||
Value
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
>(isValue: TypeGuard<Value>, example: Type): TypeGuard<Type> {
|
||||
return (arg: unknown): arg is Type => isValue(arg);
|
||||
}
|
||||
|
||||
export function isNumber(arg: unknown): arg is number {
|
||||
return typeof arg === "number"
|
||||
return typeof arg === "number";
|
||||
}
|
||||
|
||||
export function isBoolean(arg: unknown): arg is boolean {
|
||||
return typeof arg === "boolean"
|
||||
return typeof arg === "boolean";
|
||||
}
|
||||
|
||||
export function isUndefined(arg: unknown): arg is undefined {
|
||||
|
@ -113,14 +145,18 @@ export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> {
|
|||
}
|
||||
|
||||
export function toIsEnum<E>(enumDef: E): EnumTypeGuard<E> {
|
||||
return (arg): arg is EnumValue<E> => Object.values(enumDef).includes(arg as [keyof E]);
|
||||
return (arg): arg is EnumValue<E> =>
|
||||
Object.values(enumDef).includes(arg as [keyof E]);
|
||||
}
|
||||
|
||||
export function isRegExp(arg: unknown): arg is RegExp {
|
||||
return isObject(arg) && arg instanceof RegExp;
|
||||
}
|
||||
|
||||
export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) {
|
||||
export function isOptional<T>(
|
||||
arg: unknown,
|
||||
isT: TypeGuard<T>
|
||||
): arg is T | undefined {
|
||||
return arg === undefined || isT(arg);
|
||||
}
|
||||
|
||||
|
@ -160,7 +196,7 @@ export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
|
|||
|
||||
export type Statistics = {
|
||||
nodes: NodeStatistics;
|
||||
}
|
||||
};
|
||||
|
||||
export function isStatistics(arg: unknown): arg is Statistics {
|
||||
return isObject(arg) && isNodeStatistics((arg as Statistics).nodes);
|
||||
|
@ -172,9 +208,8 @@ export class CommunityConfig {
|
|||
@Field("domain") public domain: string,
|
||||
@Field("contactEmail") public contactEmail: EmailAddress,
|
||||
@ArrayField("sites", String) public sites: Site[],
|
||||
@ArrayField("domains", String) public domains: Domain[],
|
||||
) {
|
||||
}
|
||||
@ArrayField("domains", String) public domains: Domain[]
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
|
||||
|
@ -194,9 +229,8 @@ export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
|
|||
export class LegalConfig {
|
||||
constructor(
|
||||
@Field("privacyUrl", true) public privacyUrl?: Url,
|
||||
@Field("imprintUrl", true) public imprintUrl?: Url,
|
||||
) {
|
||||
}
|
||||
@Field("imprintUrl", true) public imprintUrl?: Url
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isLegalConfig(arg: unknown): arg is LegalConfig {
|
||||
|
@ -205,16 +239,12 @@ export function isLegalConfig(arg: unknown): arg is LegalConfig {
|
|||
}
|
||||
const cfg = arg as LegalConfig;
|
||||
return (
|
||||
isOptional(cfg.privacyUrl, isUrl) &&
|
||||
isOptional(cfg.imprintUrl, isUrl)
|
||||
isOptional(cfg.privacyUrl, isUrl) && isOptional(cfg.imprintUrl, isUrl)
|
||||
);
|
||||
}
|
||||
|
||||
export class ClientMapConfig {
|
||||
constructor(
|
||||
@Field("mapUrl") public mapUrl: Url,
|
||||
) {
|
||||
}
|
||||
constructor(@Field("mapUrl") public mapUrl: Url) {}
|
||||
}
|
||||
|
||||
export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
|
||||
|
@ -226,10 +256,7 @@ export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
|
|||
}
|
||||
|
||||
export class MonitoringConfig {
|
||||
constructor(
|
||||
@Field("enabled") public enabled: boolean,
|
||||
) {
|
||||
}
|
||||
constructor(@Field("enabled") public enabled: boolean) {}
|
||||
}
|
||||
|
||||
export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
|
||||
|
@ -243,9 +270,8 @@ export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
|
|||
export class CoordinatesConfig {
|
||||
constructor(
|
||||
@Field("lat") public lat: number,
|
||||
@Field("lng") public lng: number,
|
||||
) {
|
||||
}
|
||||
@Field("lng") public lng: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig {
|
||||
|
@ -253,10 +279,7 @@ export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig {
|
|||
return false;
|
||||
}
|
||||
const coords = arg as CoordinatesConfig;
|
||||
return (
|
||||
isNumber(coords.lat) &&
|
||||
isNumber(coords.lng)
|
||||
);
|
||||
return isNumber(coords.lat) && isNumber(coords.lng);
|
||||
}
|
||||
|
||||
export class CoordinatesSelectorConfig {
|
||||
|
@ -264,12 +287,13 @@ export class CoordinatesSelectorConfig {
|
|||
@Field("lat") public lat: number,
|
||||
@Field("lng") public lng: number,
|
||||
@Field("defaultZoom") public defaultZoom: number,
|
||||
@RawJsonField("layers") public layers: JSONObject,
|
||||
) {
|
||||
}
|
||||
@RawJsonField("layers") public layers: JSONObject
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isCoordinatesSelectorConfig(arg: unknown): arg is CoordinatesSelectorConfig {
|
||||
export function isCoordinatesSelectorConfig(
|
||||
arg: unknown
|
||||
): arg is CoordinatesSelectorConfig {
|
||||
if (!isObject(arg)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -286,12 +310,14 @@ export class OtherCommunityInfoConfig {
|
|||
constructor(
|
||||
@Field("showInfo") public showInfo: boolean,
|
||||
@Field("showBorderForDebugging") public showBorderForDebugging: boolean,
|
||||
@ArrayField("localCommunityPolygon", CoordinatesConfig) public localCommunityPolygon: CoordinatesConfig[],
|
||||
) {
|
||||
}
|
||||
@ArrayField("localCommunityPolygon", CoordinatesConfig)
|
||||
public localCommunityPolygon: CoordinatesConfig[]
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityInfoConfig {
|
||||
export function isOtherCommunityInfoConfig(
|
||||
arg: unknown
|
||||
): arg is OtherCommunityInfoConfig {
|
||||
if (!isObject(arg)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -309,11 +335,12 @@ export class ClientConfig {
|
|||
@Field("legal") public legal: LegalConfig,
|
||||
@Field("map") public map: ClientMapConfig,
|
||||
@Field("monitoring") public monitoring: MonitoringConfig,
|
||||
@Field("coordsSelector") public coordsSelector: CoordinatesSelectorConfig,
|
||||
@Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig,
|
||||
@Field("rootPath", true, undefined, "/") public rootPath: string,
|
||||
) {
|
||||
}
|
||||
@Field("coordsSelector")
|
||||
public coordsSelector: CoordinatesSelectorConfig,
|
||||
@Field("otherCommunityInfo")
|
||||
public otherCommunityInfo: OtherCommunityInfoConfig,
|
||||
@Field("rootPath", true, undefined, "/") public rootPath: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export function isClientConfig(arg: unknown): arg is ClientConfig {
|
||||
|
@ -345,15 +372,28 @@ export type DurationSeconds = number & { readonly __tag: unique symbol };
|
|||
export const isDurationSeconds = toIsNewtype(isNumber, NaN as DurationSeconds);
|
||||
|
||||
export type DurationMilliseconds = number & { readonly __tag: unique symbol };
|
||||
export const isDurationMilliseconds = toIsNewtype(isNumber, NaN as DurationMilliseconds);
|
||||
export const isDurationMilliseconds = toIsNewtype(
|
||||
isNumber,
|
||||
NaN as DurationMilliseconds
|
||||
);
|
||||
|
||||
export type UnixTimestampSeconds = number & { readonly __tag: unique symbol };
|
||||
export const isUnixTimestampSeconds = toIsNewtype(isNumber, NaN as UnixTimestampSeconds);
|
||||
export const isUnixTimestampSeconds = toIsNewtype(
|
||||
isNumber,
|
||||
NaN as UnixTimestampSeconds
|
||||
);
|
||||
|
||||
export type UnixTimestampMilliseconds = number & { readonly __tag: unique symbol };
|
||||
export const isUnixTimestampMilliseconds = toIsNewtype(isNumber, NaN as UnixTimestampMilliseconds);
|
||||
export type UnixTimestampMilliseconds = number & {
|
||||
readonly __tag: unique symbol;
|
||||
};
|
||||
export const isUnixTimestampMilliseconds = toIsNewtype(
|
||||
isNumber,
|
||||
NaN as UnixTimestampMilliseconds
|
||||
);
|
||||
|
||||
export function toUnixTimestampSeconds(ms: UnixTimestampMilliseconds): UnixTimestampSeconds {
|
||||
export function toUnixTimestampSeconds(
|
||||
ms: UnixTimestampMilliseconds
|
||||
): UnixTimestampSeconds {
|
||||
return Math.floor(ms) as UnixTimestampSeconds;
|
||||
}
|
||||
|
||||
|
@ -371,7 +411,7 @@ export const isMonitoringState = toIsEnum(MonitoringState);
|
|||
export type NodeId = string & { readonly __tag: unique symbol };
|
||||
export const isNodeId = toIsNewtype(isString, "" as NodeId);
|
||||
|
||||
export type Hostname = string & { readonly __tag: unique symbol }
|
||||
export type Hostname = string & { readonly __tag: unique symbol };
|
||||
export const isHostname = toIsNewtype(isString, "" as Hostname);
|
||||
|
||||
export type Nickname = string & { readonly __tag: unique symbol };
|
||||
|
@ -387,10 +427,10 @@ export type BaseNode = {
|
|||
nickname: Nickname;
|
||||
email: EmailAddress;
|
||||
hostname: Hostname;
|
||||
coords?: Coordinates;
|
||||
key?: FastdKey;
|
||||
coords: Coordinates | undefined;
|
||||
key: FastdKey | undefined;
|
||||
mac: MAC;
|
||||
}
|
||||
};
|
||||
|
||||
export function isBaseNode(arg: unknown): arg is BaseNode {
|
||||
if (!isObject(arg)) {
|
||||
|
@ -412,16 +452,14 @@ export function isBaseNode(arg: unknown): arg is BaseNode {
|
|||
*/
|
||||
export type CreateOrUpdateNode = BaseNode & {
|
||||
monitoring: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function isCreateOrUpdateNode(arg: unknown): arg is CreateOrUpdateNode {
|
||||
if (!isBaseNode(arg)) {
|
||||
return false;
|
||||
}
|
||||
const node = arg as CreateOrUpdateNode;
|
||||
return (
|
||||
isBoolean(node.monitoring)
|
||||
);
|
||||
return isBoolean(node.monitoring);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -431,7 +469,7 @@ export type StoredNode = BaseNode & {
|
|||
token: Token;
|
||||
monitoringState: MonitoringState;
|
||||
modifiedAt: UnixTimestampSeconds;
|
||||
}
|
||||
};
|
||||
|
||||
export function isStoredNode(arg: unknown): arg is StoredNode {
|
||||
if (!isObject(arg)) {
|
||||
|
@ -449,23 +487,20 @@ export function isStoredNode(arg: unknown): arg is StoredNode {
|
|||
export type NodeResponse = StoredNode & {
|
||||
monitoring: boolean;
|
||||
monitoringConfirmed: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function isNodeResponse(arg: unknown): arg is NodeResponse {
|
||||
if (!isStoredNode(arg)) {
|
||||
return false;
|
||||
}
|
||||
const node = arg as NodeResponse;
|
||||
return (
|
||||
isBoolean(node.monitoring) &&
|
||||
isBoolean(node.monitoringConfirmed)
|
||||
);
|
||||
return isBoolean(node.monitoring) && isBoolean(node.monitoringConfirmed);
|
||||
}
|
||||
|
||||
export type NodeTokenResponse = {
|
||||
token: Token;
|
||||
node: NodeResponse;
|
||||
}
|
||||
};
|
||||
|
||||
export function isNodeTokenResponse(arg: unknown): arg is NodeTokenResponse {
|
||||
if (!isObject(arg)) {
|
||||
|
@ -495,13 +530,15 @@ export const isDomain = toIsNewtype(isString, "" as Domain);
|
|||
/**
|
||||
* Represents a node in the context of a Freifunk site and domain.
|
||||
*/
|
||||
export type DomainSpecificNodeResponse = Record<NodeSortField, any> & NodeResponse & {
|
||||
site?: Site,
|
||||
domain?: Domain,
|
||||
onlineState?: OnlineState,
|
||||
}
|
||||
export type DomainSpecificNodeResponse = NodeResponse & {
|
||||
site: Site | undefined;
|
||||
domain: Domain | undefined;
|
||||
onlineState: OnlineState | undefined;
|
||||
};
|
||||
|
||||
export function isDomainSpecificNodeResponse(arg: unknown): arg is DomainSpecificNodeResponse {
|
||||
export function isDomainSpecificNodeResponse(
|
||||
arg: unknown
|
||||
): arg is DomainSpecificNodeResponse {
|
||||
if (!isNodeResponse(arg)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -514,12 +551,12 @@ export function isDomainSpecificNodeResponse(arg: unknown): arg is DomainSpecifi
|
|||
}
|
||||
|
||||
export type MonitoringResponse = {
|
||||
hostname: Hostname,
|
||||
mac: MAC,
|
||||
email: EmailAddress,
|
||||
monitoring: boolean,
|
||||
monitoringConfirmed: boolean,
|
||||
}
|
||||
hostname: Hostname;
|
||||
mac: MAC;
|
||||
email: EmailAddress;
|
||||
monitoring: boolean;
|
||||
monitoringConfirmed: boolean;
|
||||
};
|
||||
|
||||
export function isMonitoringResponse(arg: unknown): arg is MonitoringResponse {
|
||||
if (!Object(arg)) {
|
||||
|
@ -535,21 +572,32 @@ export function isMonitoringResponse(arg: unknown): arg is MonitoringResponse {
|
|||
);
|
||||
}
|
||||
|
||||
export enum NodeSortField {
|
||||
HOSTNAME = 'hostname',
|
||||
NICKNAME = 'nickname',
|
||||
EMAIL = 'email',
|
||||
TOKEN = 'token',
|
||||
MAC = 'mac',
|
||||
KEY = 'key',
|
||||
SITE = 'site',
|
||||
DOMAIN = 'domain',
|
||||
COORDS = 'coords',
|
||||
ONLINE_STATE = 'onlineState',
|
||||
MONITORING_STATE = 'monitoringState',
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
enum NodeSortFieldEnum {
|
||||
HOSTNAME = "hostname",
|
||||
NICKNAME = "nickname",
|
||||
EMAIL = "email",
|
||||
TOKEN = "token",
|
||||
MAC = "mac",
|
||||
KEY = "key",
|
||||
SITE = "site",
|
||||
DOMAIN = "domain",
|
||||
COORDS = "coords",
|
||||
ONLINE_STATE = "onlineState",
|
||||
MONITORING_STATE = "monitoringState",
|
||||
}
|
||||
|
||||
export const isNodeSortField = toIsEnum(NodeSortField);
|
||||
export type NodeSortField = keyof Pick<
|
||||
DomainSpecificNodeResponse,
|
||||
NodeSortFieldEnum
|
||||
>;
|
||||
|
||||
export function isNodeSortField(arg: unknown): arg is NodeSortField {
|
||||
if (!isString(arg)) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(NodeSortFieldEnum).includes(arg as NodeSortField);
|
||||
}
|
||||
|
||||
export type NodesFilter = {
|
||||
hasKey?: boolean;
|
||||
|
@ -558,7 +606,7 @@ export type NodesFilter = {
|
|||
site?: Site;
|
||||
domain?: Domain;
|
||||
onlineState?: OnlineState;
|
||||
}
|
||||
};
|
||||
|
||||
export const NODES_FILTER_FIELDS = {
|
||||
hasKey: Boolean,
|
||||
|
@ -588,49 +636,49 @@ export type SearchTerm = string & { readonly __tag: unique symbol }
|
|||
export const isSearchTerm = isString;
|
||||
|
||||
export enum MonitoringSortField {
|
||||
ID = 'id',
|
||||
HOSTNAME = 'hostname',
|
||||
MAC = 'mac',
|
||||
SITE = 'site',
|
||||
DOMAIN = 'domain',
|
||||
MONITORING_STATE = 'monitoring_state',
|
||||
STATE = 'state',
|
||||
LAST_SEEN = 'last_seen',
|
||||
IMPORT_TIMESTAMP = 'import_timestamp',
|
||||
LAST_STATUS_MAIL_TYPE = 'last_status_mail_type',
|
||||
LAST_STATUS_MAIL_SENT = 'last_status_mail_sent',
|
||||
CREATED_AT = 'created_at',
|
||||
MODIFIED_AT = 'modified_at',
|
||||
ID = "id",
|
||||
HOSTNAME = "hostname",
|
||||
MAC = "mac",
|
||||
SITE = "site",
|
||||
DOMAIN = "domain",
|
||||
MONITORING_STATE = "monitoring_state",
|
||||
STATE = "state",
|
||||
LAST_SEEN = "last_seen",
|
||||
IMPORT_TIMESTAMP = "import_timestamp",
|
||||
LAST_STATUS_MAIL_TYPE = "last_status_mail_type",
|
||||
LAST_STATUS_MAIL_SENT = "last_status_mail_sent",
|
||||
CREATED_AT = "created_at",
|
||||
MODIFIED_AT = "modified_at",
|
||||
}
|
||||
|
||||
export const isMonitoringSortField = toIsEnum(MonitoringSortField);
|
||||
|
||||
export enum TaskSortField {
|
||||
ID = 'id',
|
||||
NAME = 'name',
|
||||
SCHEDULE = 'schedule',
|
||||
STATE = 'state',
|
||||
RUNNING_SINCE = 'runningSince',
|
||||
LAST_RUN_STARTED = 'lastRunStarted',
|
||||
ID = "id",
|
||||
NAME = "name",
|
||||
SCHEDULE = "schedule",
|
||||
STATE = "state",
|
||||
RUNNING_SINCE = "runningSince",
|
||||
LAST_RUN_STARTED = "lastRunStarted",
|
||||
}
|
||||
|
||||
export const isTaskSortField = toIsEnum(TaskSortField);
|
||||
|
||||
export enum MailSortField {
|
||||
ID = 'id',
|
||||
FAILURES = 'failures',
|
||||
SENDER = 'sender',
|
||||
RECIPIENT = 'recipient',
|
||||
EMAIL = 'email',
|
||||
CREATED_AT = 'created_at',
|
||||
MODIFIED_AT = 'modified_at',
|
||||
ID = "id",
|
||||
FAILURES = "failures",
|
||||
SENDER = "sender",
|
||||
RECIPIENT = "recipient",
|
||||
EMAIL = "email",
|
||||
CREATED_AT = "created_at",
|
||||
MODIFIED_AT = "modified_at",
|
||||
}
|
||||
|
||||
export const isMailSortField = toIsEnum(MailSortField);
|
||||
|
||||
export type GenericSortField = {
|
||||
value: string;
|
||||
readonly __tag: unique symbol
|
||||
readonly __tag: unique symbol;
|
||||
};
|
||||
|
||||
export enum SortDirection {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import {isString, type MAC} from "../types";
|
||||
import { isString, type MAC } from "../types";
|
||||
|
||||
export function normalizeString(str: string): string {
|
||||
return isString(str) ? str.trim().replace(/\s+/g, ' ') : str;
|
||||
return isString(str) ? str.trim().replace(/\s+/g, " ") : str;
|
||||
}
|
||||
|
||||
export function normalizeMac(mac: MAC): MAC {
|
||||
// parts only contains values at odd indexes
|
||||
const parts = mac.toUpperCase().replace(/[-:]/g, '').split(/([A-F0-9]{2})/);
|
||||
const parts = mac
|
||||
.toUpperCase()
|
||||
.replace(/[-:]/g, "")
|
||||
.split(/([A-F0-9]{2})/);
|
||||
|
||||
const macParts = [];
|
||||
|
||||
|
@ -14,7 +17,7 @@ export function normalizeMac(mac: MAC): MAC {
|
|||
macParts.push(parts[i]);
|
||||
}
|
||||
|
||||
return macParts.join(':') as MAC;
|
||||
return macParts.join(":") as MAC;
|
||||
}
|
||||
|
||||
export function parseInteger(str: string): number {
|
||||
|
@ -22,6 +25,8 @@ export function parseInteger(str: string): number {
|
|||
if (parsed.toString() === str) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw new SyntaxError(`String does not represent a valid integer: "${str}"`);
|
||||
throw new SyntaxError(
|
||||
`String does not represent a valid integer: "${str}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,114 +4,114 @@
|
|||
// noinspection RegExpSimplifiable
|
||||
const CONSTRAINTS = {
|
||||
id: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[1-9][0-9]*$/,
|
||||
optional: false
|
||||
optional: false,
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
regex: /^[0-9a-fA-F]{16}$/,
|
||||
optional: false
|
||||
type: "string",
|
||||
regex: /^[0-9a-f]{16}$/i,
|
||||
optional: false,
|
||||
},
|
||||
node: {
|
||||
hostname: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[-a-z0-9_]{1,32}$/i,
|
||||
optional: false
|
||||
optional: false,
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^([a-f0-9]{64})$/i,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,
|
||||
optional: false
|
||||
optional: false,
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[-a-z0-9_ äöüß]{1,64}$/i,
|
||||
optional: false
|
||||
optional: false,
|
||||
},
|
||||
mac: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^([a-f0-9]{12}|([a-f0-9]{2}:){5}[a-f0-9]{2}|([a-f0-9]{2}-){5}[a-f0-9]{2})$/i,
|
||||
optional: false
|
||||
optional: false,
|
||||
},
|
||||
coords: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^(-?[0-9]{1,3}(\.[0-9]{1,15})? -?[0-9]{1,3}(\.[0-9]{1,15})?)$/,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
monitoring: {
|
||||
type: 'boolean',
|
||||
optional: false
|
||||
}
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
},
|
||||
},
|
||||
nodeFilters: {
|
||||
hasKey: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
},
|
||||
hasCoords: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
},
|
||||
onlineState: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^(ONLINE|OFFLINE)$/,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
monitoringState: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^(disabled|active|pending)$/,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[a-z0-9_-]{1,32}$/,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[a-z0-9_-]{1,32}$/,
|
||||
optional: true
|
||||
}
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
rest: {
|
||||
list: {
|
||||
_page: {
|
||||
type: 'number',
|
||||
type: "number",
|
||||
min: 1,
|
||||
optional: true,
|
||||
default: 1
|
||||
default: 1,
|
||||
},
|
||||
_perPage: {
|
||||
type: 'number',
|
||||
type: "number",
|
||||
min: 1,
|
||||
max: 50,
|
||||
optional: true,
|
||||
default: 20
|
||||
default: 20,
|
||||
},
|
||||
_sortDir: {
|
||||
type: 'enum',
|
||||
allowed: ['ASC', 'DESC'],
|
||||
type: "enum",
|
||||
allowed: ["ASC", "DESC"],
|
||||
optional: true,
|
||||
default: 'ASC'
|
||||
default: "ASC",
|
||||
},
|
||||
_sortField: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[a-zA-Z0-9_]{1,32}$/,
|
||||
optional: true
|
||||
optional: true,
|
||||
},
|
||||
q: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
regex: /^[äöüß a-z0-9!#$%&@:.'*+/=?^_`{|}~-]{1,64}$/i,
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
}
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default CONSTRAINTS;
|
||||
|
|
|
@ -1,23 +1,34 @@
|
|||
import {parseInteger} from "../utils/strings";
|
||||
import {isBoolean, isNumber, isObject, isOptional, isRegExp, isString, toIsArray} from "../types";
|
||||
import { parseInteger } from "../utils/strings";
|
||||
import {
|
||||
isBoolean,
|
||||
isNumber,
|
||||
isObject,
|
||||
isOptional,
|
||||
isRegExp,
|
||||
isString,
|
||||
toIsArray,
|
||||
} from "../types";
|
||||
|
||||
export interface Constraint {
|
||||
type: string,
|
||||
type: string;
|
||||
|
||||
default?: any,
|
||||
default?: unknown;
|
||||
|
||||
optional?: boolean,
|
||||
optional?: boolean;
|
||||
|
||||
allowed?: string[],
|
||||
allowed?: string[];
|
||||
|
||||
min?: number,
|
||||
max?: number,
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
regex?: RegExp,
|
||||
regex?: RegExp;
|
||||
}
|
||||
|
||||
export type Constraints = { [key: string]: Constraint };
|
||||
export type Values = { [key: string]: any };
|
||||
export type NestedConstraints = {
|
||||
[key: string]: Constraint | Constraints | NestedConstraints;
|
||||
};
|
||||
export type Values = { [key: string]: unknown };
|
||||
|
||||
export function isConstraint(arg: unknown): arg is Constraint {
|
||||
if (!isObject(arg)) {
|
||||
|
@ -36,18 +47,22 @@ export function isConstraint(arg: unknown): arg is Constraint {
|
|||
);
|
||||
}
|
||||
|
||||
export function isConstraints(constraints: unknown): constraints is Constraints {
|
||||
export function isConstraints(
|
||||
constraints: unknown
|
||||
): constraints is Constraints {
|
||||
if (!isObject(constraints)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.entries(constraints).every(([key, constraint]) => isString(key) && isConstraint(constraint));
|
||||
return Object.entries(constraints).every(
|
||||
([key, constraint]) => isString(key) && isConstraint(constraint)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
|
||||
|
||||
function isValidBoolean(value: unknown): boolean {
|
||||
return isBoolean(value) || value === 'true' || value === 'false';
|
||||
return isBoolean(value) || value === "true" || value === "false";
|
||||
}
|
||||
|
||||
function isValidNumber(constraint: Constraint, value: unknown): boolean {
|
||||
|
@ -86,7 +101,9 @@ function isValidEnum(constraint: Constraint, value: unknown): boolean {
|
|||
|
||||
function isValidString(constraint: Constraint, value: unknown): boolean {
|
||||
if (!constraint.regex) {
|
||||
throw new Error("String constraints must have regex set: " + constraint);
|
||||
throw new Error(
|
||||
"String constraints must have regex set: " + constraint
|
||||
);
|
||||
}
|
||||
|
||||
if (!isString(value)) {
|
||||
|
@ -94,32 +111,43 @@ function isValidString(constraint: Constraint, value: unknown): boolean {
|
|||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return (trimmed === '' && constraint.optional) || constraint.regex.test(trimmed);
|
||||
return (
|
||||
(trimmed === "" && constraint.optional) ||
|
||||
constraint.regex.test(trimmed)
|
||||
);
|
||||
}
|
||||
|
||||
function isValid(constraint: Constraint, acceptUndefined: boolean, value: unknown): boolean {
|
||||
function isValid(
|
||||
constraint: Constraint,
|
||||
acceptUndefined: boolean,
|
||||
value: unknown
|
||||
): boolean {
|
||||
if (value === undefined) {
|
||||
return acceptUndefined || constraint.optional === true;
|
||||
}
|
||||
|
||||
switch (constraint.type) {
|
||||
case 'boolean':
|
||||
case "boolean":
|
||||
return isValidBoolean(value);
|
||||
|
||||
case 'number':
|
||||
case "number":
|
||||
return isValidNumber(constraint, value);
|
||||
|
||||
case 'enum':
|
||||
case "enum":
|
||||
return isValidEnum(constraint, value);
|
||||
|
||||
case 'string':
|
||||
case "string":
|
||||
return isValidString(constraint, value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function areValid(constraints: Constraints, acceptUndefined: boolean, values: Values): boolean {
|
||||
function areValid(
|
||||
constraints: Constraints,
|
||||
acceptUndefined: boolean,
|
||||
values: Values
|
||||
): boolean {
|
||||
const fields = new Set(Object.keys(constraints));
|
||||
for (const field of fields) {
|
||||
if (!isValid(constraints[field], acceptUndefined, values[field])) {
|
||||
|
@ -136,10 +164,18 @@ function areValid(constraints: Constraints, acceptUndefined: boolean, values: Va
|
|||
return true;
|
||||
}
|
||||
|
||||
export function forConstraint(constraint: Constraint, acceptUndefined: boolean): (value: unknown) => boolean {
|
||||
return ((value: unknown): boolean => isValid(constraint, acceptUndefined, value));
|
||||
export function forConstraint(
|
||||
constraint: Constraint,
|
||||
acceptUndefined: boolean
|
||||
): (value: unknown) => boolean {
|
||||
return (value: unknown): boolean =>
|
||||
isValid(constraint, acceptUndefined, value);
|
||||
}
|
||||
|
||||
export function forConstraints(constraints: Constraints, acceptUndefined: boolean): (values: Values) => boolean {
|
||||
return ((values: Values): boolean => areValid(constraints, acceptUndefined, values));
|
||||
export function forConstraints(
|
||||
constraints: Constraints,
|
||||
acceptUndefined: boolean
|
||||
): (values: Values) => boolean {
|
||||
return (values: Values): boolean =>
|
||||
areValid(constraints, acceptUndefined, values);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import {ArrayField, Field, RawJsonField} from "sparkson"
|
||||
import {ClientConfig, DurationMilliseconds, isString, toIsNewtype, Url} from "../shared/types";
|
||||
import { ArrayField, Field, RawJsonField } from "sparkson";
|
||||
import {
|
||||
ClientConfig,
|
||||
DurationMilliseconds,
|
||||
isString,
|
||||
toIsNewtype,
|
||||
Url,
|
||||
} from "../shared/types";
|
||||
|
||||
export type Username = string & { readonly __tag: unique symbol };
|
||||
export const isUsername = toIsNewtype(isString, "" as Username);
|
||||
|
||||
export type CleartextPassword = string & { readonly __tag: unique symbol };
|
||||
export const isCleartextPassword = toIsNewtype(isString, "" as CleartextPassword);
|
||||
export const isCleartextPassword = toIsNewtype(
|
||||
isString,
|
||||
"" as CleartextPassword
|
||||
);
|
||||
|
||||
export type PasswordHash = string & { readonly __tag: unique symbol };
|
||||
export const isPasswordHash = toIsNewtype(isString, "" as PasswordHash);
|
||||
|
@ -13,34 +22,30 @@ export const isPasswordHash = toIsNewtype(isString, "" as PasswordHash);
|
|||
export class UsersConfig {
|
||||
constructor(
|
||||
@Field("user") public username: Username,
|
||||
@Field("passwordHash") public passwordHash: PasswordHash,
|
||||
) {
|
||||
}
|
||||
@Field("passwordHash") public passwordHash: PasswordHash
|
||||
) {}
|
||||
}
|
||||
|
||||
export class LoggingConfig {
|
||||
constructor(
|
||||
@Field("enabled") public enabled: boolean,
|
||||
@Field("debug") public debug: boolean,
|
||||
@Field("profile") public profile: boolean,
|
||||
) {
|
||||
}
|
||||
@Field("profile") public profile: boolean
|
||||
) {}
|
||||
}
|
||||
|
||||
export class InternalConfig {
|
||||
constructor(
|
||||
@Field("active") public active: boolean,
|
||||
@ArrayField("users", UsersConfig) public users: UsersConfig[],
|
||||
) {
|
||||
}
|
||||
@ArrayField("users", UsersConfig) public users: UsersConfig[]
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SMTPAuthConfig {
|
||||
constructor(
|
||||
@Field("user") public user: Username,
|
||||
@Field("pass") public pass: CleartextPassword,
|
||||
) {
|
||||
}
|
||||
@Field("pass") public pass: CleartextPassword
|
||||
) {}
|
||||
}
|
||||
|
||||
// For details see: https://nodemailer.com/smtp/
|
||||
|
@ -55,26 +60,24 @@ export class SMTPConfig {
|
|||
@Field("opportunisticTLS") public opportunisticTLS?: boolean,
|
||||
@Field("name") public name?: string,
|
||||
@Field("localAddress") public localAddress?: string,
|
||||
@Field("connectionTimeout") public connectionTimeout?: DurationMilliseconds,
|
||||
@Field("connectionTimeout")
|
||||
public connectionTimeout?: DurationMilliseconds,
|
||||
@Field("greetingTimeout") public greetingTimeout?: DurationMilliseconds,
|
||||
@Field("socketTimeout") public socketTimeout?: DurationMilliseconds,
|
||||
) {
|
||||
}
|
||||
@Field("socketTimeout") public socketTimeout?: DurationMilliseconds
|
||||
) {}
|
||||
}
|
||||
|
||||
export class EmailConfig {
|
||||
constructor(
|
||||
@Field("from") public from: string,
|
||||
@RawJsonField("smtp") public smtp: SMTPConfig,
|
||||
) {
|
||||
}
|
||||
@RawJsonField("smtp") public smtp: SMTPConfig
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ServerMapConfig {
|
||||
constructor(
|
||||
@ArrayField("nodesJsonUrl", String) public nodesJsonUrl: Url[],
|
||||
) {
|
||||
}
|
||||
@ArrayField("nodesJsonUrl", String) public nodesJsonUrl: Url[]
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ServerConfig {
|
||||
|
@ -87,15 +90,13 @@ export class ServerConfig {
|
|||
@Field("internal") public internal: InternalConfig,
|
||||
@Field("email") public email: EmailConfig,
|
||||
@Field("map") public map: ServerMapConfig,
|
||||
@Field("rootPath", true, undefined, "/") public rootPath: string,
|
||||
) {
|
||||
}
|
||||
@Field("rootPath", true, undefined, "/") public rootPath: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Config {
|
||||
constructor(
|
||||
@Field("server") public server: ServerConfig,
|
||||
@Field("client") public client: ClientConfig,
|
||||
) {
|
||||
}
|
||||
@Field("client") public client: ClientConfig
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -1,51 +1,70 @@
|
|||
import {ISqlite, Statement} from "sqlite";
|
||||
import { ISqlite, Statement } from "sqlite";
|
||||
|
||||
export type RunResult = ISqlite.RunResult;
|
||||
export type SqlType = ISqlite.SqlType;
|
||||
|
||||
export {Statement};
|
||||
export { Statement };
|
||||
|
||||
export interface TypedDatabase {
|
||||
/**
|
||||
* @see Database.on
|
||||
*/
|
||||
on(event: string, listener: any): Promise<void>;
|
||||
on(event: string, listener: unknown): Promise<void>;
|
||||
|
||||
/**
|
||||
* @see Database.run
|
||||
*/
|
||||
run(sql: SqlType, ...params: any[]): Promise<RunResult>;
|
||||
run(sql: SqlType, ...params: unknown[]): Promise<RunResult>;
|
||||
|
||||
/**
|
||||
* @see Database.get
|
||||
*/
|
||||
get<T>(sql: SqlType, ...params: any[]): Promise<T | undefined>;
|
||||
get<T>(sql: SqlType, ...params: unknown[]): Promise<T | undefined>;
|
||||
|
||||
/**
|
||||
* @see Database.each
|
||||
*/
|
||||
each<T>(sql: SqlType, callback: (err: any, row: T) => void): Promise<number>;
|
||||
each<T>(
|
||||
sql: SqlType,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
|
||||
each<T>(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
|
||||
each<T>(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
|
||||
each<T>(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise<number>;
|
||||
each<T>(
|
||||
sql: SqlType,
|
||||
param1: unknown,
|
||||
param2: unknown,
|
||||
param3: unknown,
|
||||
callback: (err: unknown, row: T) => void
|
||||
): Promise<number>;
|
||||
|
||||
each<T>(sql: SqlType, ...params: any[]): Promise<number>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
each<T>(sql: SqlType, ...params: unknown[]): Promise<number>;
|
||||
|
||||
/**
|
||||
* @see Database.all
|
||||
*/
|
||||
all<T = never>(sql: SqlType, ...params: any[]): Promise<T[]>;
|
||||
all<T = never>(sql: SqlType, ...params: unknown[]): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* @see Database.exec
|
||||
*/
|
||||
exec(sql: SqlType, ...params: any[]): Promise<void>;
|
||||
exec(sql: SqlType, ...params: unknown[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* @see Database.prepare
|
||||
*/
|
||||
prepare(sql: SqlType, ...params: any[]): Promise<Statement>;
|
||||
prepare(sql: SqlType, ...params: unknown[]): Promise<Statement>;
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ export * from "./logger";
|
|||
export * from "../shared/types";
|
||||
|
||||
export type NodeStateData = {
|
||||
site?: Site,
|
||||
domain?: Domain,
|
||||
state: OnlineState,
|
||||
}
|
||||
site?: Site;
|
||||
domain?: Domain;
|
||||
state: OnlineState;
|
||||
};
|
||||
|
||||
export function toCreateOrUpdateNode(node: StoredNode): CreateOrUpdateNode {
|
||||
return {
|
||||
|
@ -37,7 +37,7 @@ export function toCreateOrUpdateNode(node: StoredNode): CreateOrUpdateNode {
|
|||
key: node.key,
|
||||
mac: node.mac,
|
||||
monitoring: node.monitoringState !== MonitoringState.DISABLED,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeResponse(node: StoredNode): NodeResponse {
|
||||
|
@ -53,17 +53,20 @@ export function toNodeResponse(node: StoredNode): NodeResponse {
|
|||
monitoringConfirmed: node.monitoringState === MonitoringState.ACTIVE,
|
||||
monitoringState: node.monitoringState,
|
||||
modifiedAt: node.modifiedAt,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toNodeTokenResponse(node: StoredNode): NodeTokenResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
node: toNodeResponse(node),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toDomainSpecificNodeResponse(node: StoredNode, nodeStateData: NodeStateData): DomainSpecificNodeResponse {
|
||||
export function toDomainSpecificNodeResponse(
|
||||
node: StoredNode,
|
||||
nodeStateData: NodeStateData
|
||||
): DomainSpecificNodeResponse {
|
||||
return {
|
||||
token: node.token,
|
||||
nickname: node.nickname,
|
||||
|
@ -79,7 +82,7 @@ export function toDomainSpecificNodeResponse(node: StoredNode, nodeStateData: No
|
|||
site: nodeStateData.site,
|
||||
domain: nodeStateData.domain,
|
||||
onlineState: nodeStateData.state,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toMonitoringResponse(node: StoredNode): MonitoringResponse {
|
||||
|
@ -93,7 +96,7 @@ export function toMonitoringResponse(node: StoredNode): MonitoringResponse {
|
|||
}
|
||||
|
||||
export type NodeSecrets = {
|
||||
monitoringToken?: MonitoringToken,
|
||||
monitoringToken?: MonitoringToken;
|
||||
};
|
||||
|
||||
export type MailId = number & { readonly __tag: unique symbol };
|
||||
|
@ -118,4 +121,4 @@ export type Mail = {
|
|||
recipient: EmailAddress;
|
||||
data: MailData;
|
||||
failures: number;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'profile';
|
||||
export const LogLevels: LogLevel[] = ['debug', 'info', 'warn', 'error', 'profile'];
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error" | "profile";
|
||||
export const LogLevels: LogLevel[] = [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"profile",
|
||||
];
|
||||
|
||||
export function isLogLevel(arg: any): arg is LogLevel {
|
||||
export function isLogLevel(arg: unknown): arg is LogLevel {
|
||||
if (typeof arg !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
@ -14,12 +20,12 @@ export function isLogLevel(arg: any): arg is LogLevel {
|
|||
}
|
||||
|
||||
export interface TaggedLogger {
|
||||
log(level: LogLevel, ...args: any[]): void;
|
||||
debug(...args: any[]): void;
|
||||
info(...args: any[]): void;
|
||||
warn(...args: any[]): void;
|
||||
error(...args: any[]): void;
|
||||
profile(...args: any[]): void;
|
||||
log(level: LogLevel, ...args: unknown[]): void;
|
||||
debug(...args: unknown[]): void;
|
||||
info(...args: unknown[]): void;
|
||||
warn(...args: unknown[]): void;
|
||||
error(...args: unknown[]): void;
|
||||
profile(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import _ from "lodash";
|
||||
|
||||
export function inCondition<T>(field: string, list: T[]): {query: string, params: T[]} {
|
||||
export function inCondition<T>(
|
||||
field: string,
|
||||
list: T[]
|
||||
): { query: string; params: T[] } {
|
||||
return {
|
||||
query: '(' + field + ' IN (' + _.times(list.length, () =>'?').join(', ') + '))',
|
||||
query:
|
||||
"(" +
|
||||
field +
|
||||
" IN (" +
|
||||
_.times(list.length, () => "?").join(", ") +
|
||||
"))",
|
||||
params: list,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
badRequest: {code: 400},
|
||||
notFound: {code: 404},
|
||||
conflict: {code: 409},
|
||||
internalError: {code: 500}
|
||||
}
|
6
server/utils/errorTypes.ts
Normal file
6
server/utils/errorTypes.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
badRequest: { code: 400 },
|
||||
notFound: { code: 404 },
|
||||
conflict: { code: 409 },
|
||||
internalError: { code: 500 },
|
||||
};
|
|
@ -1,14 +1,20 @@
|
|||
import _ from "lodash";
|
||||
|
||||
import CONSTRAINTS from "../shared/validation/constraints";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
import ErrorTypes from "./errorTypes";
|
||||
import Logger from "../logger";
|
||||
import {Constraints, forConstraints, isConstraints} from "../shared/validation/validator";
|
||||
import {Request, Response} from "express";
|
||||
import {
|
||||
Constraints,
|
||||
forConstraints,
|
||||
isConstraints,
|
||||
NestedConstraints,
|
||||
} from "../shared/validation/validator";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
EnumTypeGuard,
|
||||
EnumValue,
|
||||
type GenericSortField,
|
||||
getFieldIfExists,
|
||||
isJSONObject,
|
||||
isNumber,
|
||||
isString,
|
||||
|
@ -16,13 +22,13 @@ import {
|
|||
JSONObject,
|
||||
JSONValue,
|
||||
SortDirection,
|
||||
TypeGuard
|
||||
TypeGuard,
|
||||
} from "../types";
|
||||
|
||||
export type RequestData = JSONObject;
|
||||
export type RequestHandler = (request: Request, response: Response) => void;
|
||||
|
||||
export type Entity = { [key: string]: any };
|
||||
export type Entity = { [key: string]: unknown };
|
||||
|
||||
export type RestParams = {
|
||||
q?: string;
|
||||
|
@ -36,21 +42,36 @@ export type RestParams = {
|
|||
filters?: FilterClause;
|
||||
};
|
||||
|
||||
export type OrderByClause = { query: string, params: any[] };
|
||||
export type LimitOffsetClause = { query: string, params: any[] };
|
||||
export type FilterClause = { query: string, params: any[] };
|
||||
export type OrderByClause = { query: string; params: unknown[] };
|
||||
export type LimitOffsetClause = { query: string; params: unknown[] };
|
||||
export type FilterClause = { query: string; params: unknown[] };
|
||||
|
||||
function respond(res: Response, httpCode: number, data: string, type: "html"): void;
|
||||
function respond(res: Response, httpCode: number, data: JSONValue, type: "json"): void;
|
||||
function respond(res: Response, httpCode: number, data: JSONValue, type: "html" | "json"): void {
|
||||
function respond(
|
||||
res: Response,
|
||||
httpCode: number,
|
||||
data: string,
|
||||
type: "html"
|
||||
): void;
|
||||
function respond(
|
||||
res: Response,
|
||||
httpCode: number,
|
||||
data: JSONValue,
|
||||
type: "json"
|
||||
): void;
|
||||
function respond(
|
||||
res: Response,
|
||||
httpCode: number,
|
||||
data: JSONValue,
|
||||
type: "html" | "json"
|
||||
): void {
|
||||
switch (type) {
|
||||
case 'html':
|
||||
res.writeHead(httpCode, {'Content-Type': 'text/html'});
|
||||
case "html":
|
||||
res.writeHead(httpCode, { "Content-Type": "text/html" });
|
||||
res.end(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
res.writeHead(httpCode, {'Content-Type': 'application/json'});
|
||||
res.writeHead(httpCode, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(data));
|
||||
break;
|
||||
}
|
||||
|
@ -59,16 +80,22 @@ function respond(res: Response, httpCode: number, data: JSONValue, type: "html"
|
|||
function orderByClause<S>(
|
||||
restParams: RestParams,
|
||||
defaultSortField: EnumValue<S>,
|
||||
isSortField: EnumTypeGuard<S>,
|
||||
isSortField: EnumTypeGuard<S>
|
||||
): OrderByClause {
|
||||
let sortField: EnumValue<S> | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined;
|
||||
let sortField: EnumValue<S> | undefined = isSortField(restParams._sortField)
|
||||
? restParams._sortField
|
||||
: undefined;
|
||||
if (!sortField) {
|
||||
sortField = defaultSortField;
|
||||
}
|
||||
|
||||
return {
|
||||
query: 'ORDER BY LOWER(' + sortField + ') ' + (restParams._sortDir === SortDirection.ASCENDING ? 'ASC' : 'DESC'),
|
||||
params: []
|
||||
query:
|
||||
"ORDER BY LOWER(" +
|
||||
sortField +
|
||||
") " +
|
||||
(restParams._sortDir === SortDirection.ASCENDING ? "ASC" : "DESC"),
|
||||
params: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,55 +104,64 @@ function limitOffsetClause(restParams: RestParams): LimitOffsetClause {
|
|||
const perPage = restParams._perPage;
|
||||
|
||||
return {
|
||||
query: 'LIMIT ? OFFSET ?',
|
||||
params: [perPage, ((page - 1) * perPage)]
|
||||
query: "LIMIT ? OFFSET ?",
|
||||
params: [perPage, (page - 1) * perPage],
|
||||
};
|
||||
}
|
||||
|
||||
function escapeForLikePattern(str: string): string {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/%/g, '\\%')
|
||||
.replace(/_/g, '\\_');
|
||||
return str.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
||||
}
|
||||
|
||||
function filterCondition(restParams: RestParams, filterFields: string[]): FilterClause {
|
||||
function filterCondition(
|
||||
restParams: RestParams,
|
||||
filterFields: string[]
|
||||
): FilterClause {
|
||||
if (_.isEmpty(filterFields)) {
|
||||
return {
|
||||
query: '1 = 1',
|
||||
params: []
|
||||
query: "1 = 1",
|
||||
params: [],
|
||||
};
|
||||
}
|
||||
|
||||
let query = filterFields
|
||||
.map(field => 'LOWER(' + field + ') LIKE ?')
|
||||
.join(' OR ');
|
||||
.map((field) => "LOWER(" + field + ") LIKE ?")
|
||||
.join(" OR ");
|
||||
|
||||
query += ' ESCAPE \'\\\'';
|
||||
query += " ESCAPE '\\'";
|
||||
|
||||
const search = '%' + (isString(restParams.q) ? escapeForLikePattern(restParams.q.trim().toLowerCase()) : '') + '%';
|
||||
const search =
|
||||
"%" +
|
||||
(isString(restParams.q)
|
||||
? escapeForLikePattern(restParams.q.trim().toLowerCase())
|
||||
: "") +
|
||||
"%";
|
||||
const params = _.times(filterFields.length, () => search);
|
||||
|
||||
return {
|
||||
query: query,
|
||||
params: params
|
||||
params: params,
|
||||
};
|
||||
}
|
||||
|
||||
function getConstrainedValues(data: { [key: string]: any }, constraints: Constraints): { [key: string]: any } {
|
||||
const values: { [key: string]: any } = {};
|
||||
function getConstrainedValues(
|
||||
data: { [key: string]: unknown },
|
||||
constraints: Constraints
|
||||
): { [key: string]: unknown } {
|
||||
const values: { [key: string]: unknown } = {};
|
||||
for (const key of Object.keys(constraints)) {
|
||||
const value = data[key];
|
||||
values[key] =
|
||||
isUndefined(value) && key in constraints && !isUndefined(constraints[key].default)
|
||||
isUndefined(value) &&
|
||||
key in constraints &&
|
||||
!isUndefined(constraints[key].default)
|
||||
? constraints[key].default
|
||||
: value;
|
||||
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function normalize(data: any): JSONObject {
|
||||
function normalize(data: unknown): JSONObject {
|
||||
return isJSONObject(data) ? data : {};
|
||||
}
|
||||
|
||||
|
@ -144,25 +180,31 @@ export function getData(req: Request): RequestData {
|
|||
export async function getValidRestParams(
|
||||
type: string,
|
||||
subtype: string | null,
|
||||
req: Request,
|
||||
req: Request
|
||||
): Promise<RestParams> {
|
||||
const restConstraints = CONSTRAINTS.rest as { [key: string]: any };
|
||||
let constraints: Constraints;
|
||||
const restConstraints = CONSTRAINTS.rest as { [key: string]: Constraints };
|
||||
if (!(type in restConstraints) || !isConstraints(restConstraints[type])) {
|
||||
Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type);
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
Logger.tag("validation", "rest").error(
|
||||
"Unknown REST resource type: {}",
|
||||
type
|
||||
);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
constraints = restConstraints[type];
|
||||
const constraints: Constraints = restConstraints[type];
|
||||
|
||||
let filterConstraints: Constraints = {};
|
||||
if (subtype) {
|
||||
const subtypeFilters = subtype + 'Filters';
|
||||
const constraintsObj = CONSTRAINTS as { [key: string]: any };
|
||||
if (!(subtypeFilters in constraintsObj) || !isConstraints(constraintsObj[subtypeFilters])) {
|
||||
Logger.tag('validation', 'rest').error('Unknown REST resource subtype: {}', subtype);
|
||||
throw {data: 'Internal error.', type: ErrorTypes.internalError};
|
||||
const subtypeFilters = subtype + "Filters";
|
||||
const nestedConstraints = CONSTRAINTS as NestedConstraints;
|
||||
const subConstraints = nestedConstraints[subtypeFilters];
|
||||
if (!isConstraints(subConstraints)) {
|
||||
Logger.tag("validation", "rest").error(
|
||||
"Unknown REST resource subtype: {}",
|
||||
subtype
|
||||
);
|
||||
throw { data: "Internal error.", type: ErrorTypes.internalError };
|
||||
}
|
||||
filterConstraints = constraintsObj[subtypeFilters];
|
||||
filterConstraints = subConstraints;
|
||||
}
|
||||
|
||||
const data = getData(req);
|
||||
|
@ -173,20 +215,24 @@ export async function getValidRestParams(
|
|||
const areValidParams = forConstraints(constraints, false);
|
||||
const areValidFilters = forConstraints(filterConstraints, false);
|
||||
if (!areValidParams(restParams) || !areValidFilters(filterParams)) {
|
||||
throw {data: 'Invalid REST parameters.', type: ErrorTypes.badRequest};
|
||||
throw { data: "Invalid REST parameters.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
restParams.filters = filterParams;
|
||||
return restParams as RestParams;
|
||||
}
|
||||
|
||||
export function filter<E>(entities: E[], allowedFilterFields: string[], restParams: RestParams): E[] {
|
||||
export function filter<E>(
|
||||
entities: E[],
|
||||
allowedFilterFields: string[],
|
||||
restParams: RestParams
|
||||
): E[] {
|
||||
let query = restParams.q;
|
||||
if (query) {
|
||||
query = query.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function queryMatches(entity: Entity): boolean {
|
||||
function queryMatches(entity: E): boolean {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
@ -194,7 +240,7 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
|
|||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
let value = entity[field];
|
||||
let value = getFieldIfExists(entity, field);
|
||||
if (isNumber(value)) {
|
||||
value = value.toString();
|
||||
}
|
||||
|
@ -203,18 +249,21 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
|
|||
return false;
|
||||
}
|
||||
|
||||
value = value.toLowerCase();
|
||||
if (field === 'mac') {
|
||||
return _.includes(value.replace(/:/g, ''), query.replace(/:/g, ''));
|
||||
const lowerCaseValue = value.toLowerCase();
|
||||
if (field === "mac") {
|
||||
return _.includes(
|
||||
lowerCaseValue.replace(/:/g, ""),
|
||||
query.replace(/:/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
return _.includes(value, query);
|
||||
return _.includes(lowerCaseValue, query);
|
||||
});
|
||||
}
|
||||
|
||||
const filters = restParams.filters;
|
||||
|
||||
function filtersMatch(entity: Entity): boolean {
|
||||
function filtersMatch(entity: E): boolean {
|
||||
if (isUndefined(filters) || _.isEmpty(filters)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -223,19 +272,36 @@ export function filter<E>(entities: E[], allowedFilterFields: string[], restPara
|
|||
if (isUndefined(value)) {
|
||||
return true;
|
||||
}
|
||||
if (key.startsWith('has')) {
|
||||
const entityKey = key.substring(3, 4).toLowerCase() + key.substring(4);
|
||||
return _.isEmpty(entity[entityKey]).toString() !== value;
|
||||
if (key.startsWith("has")) {
|
||||
const entityKey =
|
||||
key.substring(3, 4).toLowerCase() + key.substring(4);
|
||||
return (
|
||||
_.isEmpty(
|
||||
getFieldIfExists(entity, entityKey)
|
||||
).toString() !== value
|
||||
);
|
||||
}
|
||||
return entity[key] === value;
|
||||
return getFieldIfExists(entity, key) === value;
|
||||
});
|
||||
}
|
||||
|
||||
return entities.filter(entity => queryMatches(entity) && filtersMatch(entity));
|
||||
return entities.filter(
|
||||
(entity) => queryMatches(entity) && filtersMatch(entity)
|
||||
);
|
||||
}
|
||||
|
||||
export function sort<T extends Record<S, any>, S extends string>(entities: T[], isSortField: TypeGuard<S>, restParams: RestParams): T[] {
|
||||
const sortField: S | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined;
|
||||
export function sort<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Type extends { [Key in SortField]: any },
|
||||
SortField extends string
|
||||
>(
|
||||
entities: Type[],
|
||||
isSortField: TypeGuard<SortField>,
|
||||
restParams: RestParams
|
||||
): Type[] {
|
||||
const sortField: SortField | undefined = isSortField(restParams._sortField)
|
||||
? restParams._sortField
|
||||
: undefined;
|
||||
if (!sortField) {
|
||||
return entities;
|
||||
}
|
||||
|
@ -259,69 +325,74 @@ export function sort<T extends Record<S, any>, S extends string>(entities: T[],
|
|||
order = 1;
|
||||
}
|
||||
|
||||
return restParams._sortDir === SortDirection.DESCENDING ? -order : order;
|
||||
return restParams._sortDir === SortDirection.DESCENDING
|
||||
? -order
|
||||
: order;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export function getPageEntities<Entity>(entities: Entity[], restParams: RestParams): Entity[] {
|
||||
export function getPageEntities<Entity>(
|
||||
entities: Entity[],
|
||||
restParams: RestParams
|
||||
): Entity[] {
|
||||
const page = restParams._page;
|
||||
const perPage = restParams._perPage;
|
||||
|
||||
return entities.slice((page - 1) * perPage, page * perPage);
|
||||
}
|
||||
|
||||
export {filterCondition as whereCondition};
|
||||
export { filterCondition as whereCondition };
|
||||
|
||||
export function filterClause<S>(
|
||||
restParams: RestParams,
|
||||
defaultSortField: EnumValue<S>,
|
||||
isSortField: EnumTypeGuard<S>,
|
||||
filterFields: string[],
|
||||
filterFields: string[]
|
||||
): FilterClause {
|
||||
const orderBy = orderByClause<S>(
|
||||
restParams,
|
||||
defaultSortField,
|
||||
isSortField,
|
||||
);
|
||||
const orderBy = orderByClause<S>(restParams, defaultSortField, isSortField);
|
||||
const limitOffset = limitOffsetClause(restParams);
|
||||
|
||||
const filter = filterCondition(
|
||||
restParams,
|
||||
filterFields
|
||||
);
|
||||
const filter = filterCondition(restParams, filterFields);
|
||||
|
||||
return {
|
||||
query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query,
|
||||
params: [...filter.params, ...orderBy.params, ...limitOffset.params]
|
||||
query: filter.query + " " + orderBy.query + " " + limitOffset.query,
|
||||
params: [...filter.params, ...orderBy.params, ...limitOffset.params],
|
||||
};
|
||||
}
|
||||
|
||||
export function success(res: Response, data: JSONValue) {
|
||||
respond(res, 200, data, 'json');
|
||||
respond(res, 200, data, "json");
|
||||
}
|
||||
|
||||
export function successHtml(res: Response, html: string) {
|
||||
respond(res, 200, html, 'html');
|
||||
respond(res, 200, html, "html");
|
||||
}
|
||||
|
||||
export function error(res: Response, err: { data: JSONValue, type: { code: number } }) {
|
||||
respond(res, err.type.code, err.data, 'json');
|
||||
export function error(
|
||||
res: Response,
|
||||
err: { data: JSONValue; type: { code: number } }
|
||||
) {
|
||||
respond(res, err.type.code, err.data, "json");
|
||||
}
|
||||
|
||||
export function handleJSON<Response>(handler: () => Promise<Response>): RequestHandler {
|
||||
export function handleJSON<Response>(
|
||||
handler: () => Promise<Response>
|
||||
): RequestHandler {
|
||||
return (request, response) => {
|
||||
handler()
|
||||
.then(data => success(response, data || {}))
|
||||
.catch(e => error(response, e));
|
||||
.then((data) => success(response, data || {}))
|
||||
.catch((e) => error(response, e));
|
||||
};
|
||||
}
|
||||
|
||||
export function handleJSONWithData<Response>(handler: (data: RequestData) => Promise<Response>): RequestHandler {
|
||||
export function handleJSONWithData<Response>(
|
||||
handler: (data: RequestData) => Promise<Response>
|
||||
): RequestHandler {
|
||||
return (request, response) => {
|
||||
handler(getData(request))
|
||||
.then(data => success(response, data || {}))
|
||||
.catch(e => error(response, e));
|
||||
.then((data) => success(response, data || {}))
|
||||
.catch((e) => error(response, e));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {parseTimestamp} from "./time";
|
||||
import { parseTimestamp } from "./time";
|
||||
import moment from "moment";
|
||||
|
||||
const TIMESTAMP_INVALID_STRING = "2020-01-02T42:99:23.000Z";
|
||||
const TIMESTAMP_VALID_STRING = "2020-01-02T12:34:56.000Z";
|
||||
|
||||
test('parseTimestamp() should fail parsing non-string timestamp', () => {
|
||||
test("parseTimestamp() should fail parsing non-string timestamp", () => {
|
||||
// given
|
||||
const timestamp = {};
|
||||
|
||||
|
@ -15,7 +15,7 @@ test('parseTimestamp() should fail parsing non-string timestamp', () => {
|
|||
expect(parsedTimestamp).toEqual(null);
|
||||
});
|
||||
|
||||
test('parseTimestamp() should fail parsing empty timestamp string', () => {
|
||||
test("parseTimestamp() should fail parsing empty timestamp string", () => {
|
||||
// given
|
||||
const timestamp = "";
|
||||
|
||||
|
@ -26,7 +26,7 @@ test('parseTimestamp() should fail parsing empty timestamp string', () => {
|
|||
expect(parsedTimestamp).toEqual(null);
|
||||
});
|
||||
|
||||
test('parseTimestamp() should fail parsing invalid timestamp string', () => {
|
||||
test("parseTimestamp() should fail parsing invalid timestamp string", () => {
|
||||
// given
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const timestamp = TIMESTAMP_INVALID_STRING;
|
||||
|
@ -38,7 +38,7 @@ test('parseTimestamp() should fail parsing invalid timestamp string', () => {
|
|||
expect(parsedTimestamp).toEqual(null);
|
||||
});
|
||||
|
||||
test('parseTimestamp() should succeed parsing valid timestamp string', () => {
|
||||
test("parseTimestamp() should succeed parsing valid timestamp string", () => {
|
||||
// given
|
||||
const timestamp = TIMESTAMP_VALID_STRING;
|
||||
|
||||
|
@ -47,7 +47,7 @@ test('parseTimestamp() should succeed parsing valid timestamp string', () => {
|
|||
|
||||
// then
|
||||
if (parsedTimestamp === null) {
|
||||
fail('timestamp should not be null');
|
||||
fail("timestamp should not be null");
|
||||
}
|
||||
expect(moment.unix(parsedTimestamp).toISOString()).toEqual(timestamp);
|
||||
});
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import {DurationSeconds, isString, UnixTimestampSeconds} from "../types";
|
||||
import moment, {Moment} from "moment";
|
||||
import { DurationSeconds, isString, UnixTimestampSeconds } from "../types";
|
||||
import moment, { Moment } from "moment";
|
||||
|
||||
export function now(): UnixTimestampSeconds {
|
||||
return Math.round(Date.now() / 1000.0) as UnixTimestampSeconds;
|
||||
}
|
||||
|
||||
export function subtract(timestamp: UnixTimestampSeconds, duration: DurationSeconds): UnixTimestampSeconds {
|
||||
export function subtract(
|
||||
timestamp: UnixTimestampSeconds,
|
||||
duration: DurationSeconds
|
||||
): UnixTimestampSeconds {
|
||||
return (timestamp - duration) as UnixTimestampSeconds;
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,9 @@ export function formatTimestamp(timestamp: UnixTimestampSeconds): string {
|
|||
return moment.unix(timestamp).format();
|
||||
}
|
||||
|
||||
export function parseTimestamp(timestamp: any): UnixTimestampSeconds | null {
|
||||
export function parseTimestamp(
|
||||
timestamp: unknown
|
||||
): UnixTimestampSeconds | null {
|
||||
if (!isString(timestamp)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -53,4 +58,3 @@ export function parseTimestamp(timestamp: any): UnixTimestampSeconds | null {
|
|||
}
|
||||
return unix(parsed);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,34 @@
|
|||
import {config} from "../config"
|
||||
import {MonitoringToken, Url} from "../types"
|
||||
import { config } from "../config";
|
||||
import { MonitoringToken, Url } from "../types";
|
||||
|
||||
function formUrl(route: string, queryParams?: { [key: string]: string }): Url {
|
||||
let url = config.server.baseUrl as string;
|
||||
if (route || queryParams) {
|
||||
url += '/#/';
|
||||
url += "/#/";
|
||||
}
|
||||
if (route) {
|
||||
url += route;
|
||||
}
|
||||
if (queryParams) {
|
||||
url += '?';
|
||||
url +=
|
||||
Object.entries(queryParams)
|
||||
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
|
||||
.join("&");
|
||||
url += "?";
|
||||
url += Object.entries(queryParams)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
encodeURIComponent(key) + "=" + encodeURIComponent(value)
|
||||
)
|
||||
.join("&");
|
||||
}
|
||||
return url as Url;
|
||||
}
|
||||
|
||||
export function editNodeUrl(): Url {
|
||||
return formUrl('update');
|
||||
return formUrl("update");
|
||||
}
|
||||
|
||||
export function monitoringConfirmUrl(monitoringToken: MonitoringToken): Url {
|
||||
return formUrl('monitoring/confirm', {token: monitoringToken});
|
||||
return formUrl("monitoring/confirm", { token: monitoringToken });
|
||||
}
|
||||
|
||||
export function monitoringDisableUrl(monitoringToken: MonitoringToken): Url {
|
||||
return formUrl('monitoring/disable', {token: monitoringToken});
|
||||
return formUrl("monitoring/disable", { token: monitoringToken });
|
||||
}
|
||||
|
|
511
yarn.lock
511
yarn.lock
|
@ -368,11 +368,45 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@eslint/eslintrc@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f"
|
||||
integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^9.3.2"
|
||||
globals "^13.15.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@gar/promisify@^1.0.1":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
||||
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
|
||||
integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
"@humanwhocodes/gitignore-to-minimatch@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
|
||||
integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
|
||||
|
||||
"@humanwhocodes/object-schema@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
|
@ -701,6 +735,11 @@
|
|||
mkdirp "^1.0.4"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
"@rushstack/eslint-patch@^1.1.4":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27"
|
||||
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==
|
||||
|
||||
"@selderee/plugin-htmlparser2@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d"
|
||||
|
@ -903,6 +942,11 @@
|
|||
expect "^28.0.0"
|
||||
pretty-format "^28.0.0"
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/lodash-es@^4.17.6":
|
||||
version "4.17.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0"
|
||||
|
@ -1017,6 +1061,103 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz#9639020e1fb65120a2f4387db8f1e8b63efdf229"
|
||||
integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.0.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.34.0.tgz#d690f60e335596f38b01792e8f4b361d9bd0cb35"
|
||||
integrity sha512-eRfPPcasO39iwjlUAMtjeueRGuIrW3TQ9WseIDl7i5UWuFbf83yYaU7YPs4j8+4CxUMIsj1k+4kV+E+G+6ypDQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.34.0"
|
||||
"@typescript-eslint/type-utils" "5.34.0"
|
||||
"@typescript-eslint/utils" "5.34.0"
|
||||
debug "^4.3.4"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
ignore "^5.2.0"
|
||||
regexpp "^3.2.0"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.0.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.34.0.tgz#ca710858ea85dbfd30c9b416a335dc49e82dbc07"
|
||||
integrity sha512-SZ3NEnK4usd2CXkoV3jPa/vo1mWX1fqRyIVUQZR4As1vyp4fneknBNJj+OFtV8WAVgGf+rOHMSqQbs2Qn3nFZQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.34.0"
|
||||
"@typescript-eslint/types" "5.34.0"
|
||||
"@typescript-eslint/typescript-estree" "5.34.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.34.0.tgz#14efd13dc57602937e25f188fd911f118781e527"
|
||||
integrity sha512-HNvASMQlah5RsBW6L6c7IJ0vsm+8Sope/wu5sEAf7joJYWNb1LDbJipzmdhdUOnfrDFE6LR1j57x1EYVxrY4ow==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.34.0"
|
||||
"@typescript-eslint/visitor-keys" "5.34.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.34.0.tgz#7a324ab9ddd102cd5e1beefc94eea6f3eb32d32d"
|
||||
integrity sha512-Pxlno9bjsQ7hs1pdWRUv9aJijGYPYsHpwMeCQ/Inavhym3/XaKt1ZKAA8FIw4odTBfowBdZJDMxf2aavyMDkLg==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.34.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.34.0.tgz#217bf08049e9e7b86694d982e88a2c1566330c78"
|
||||
integrity sha512-49fm3xbbUPuzBIOcy2CDpYWqy/X7VBkxVN+DC21e0zIm3+61Z0NZi6J9mqPmSW1BDVk9FIOvuCFyUPjXz93sjA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.34.0.tgz#ba7b83f4bf8ccbabf074bbf1baca7a58de3ccb9a"
|
||||
integrity sha512-mXHAqapJJDVzxauEkfJI96j3D10sd567LlqroyCeJaHnu42sDbjxotGb3XFtGPYKPD9IyLjhsoULML1oI3M86A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.34.0"
|
||||
"@typescript-eslint/visitor-keys" "5.34.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.34.0.tgz#0cae98f48d8f9e292e5caa9343611b6faf49e743"
|
||||
integrity sha512-kWRYybU4Rn++7lm9yu8pbuydRyQsHRoBDIo11k7eqBWTldN4xUdVUMCsHBiE7aoEkFzrUEaZy3iH477vr4xHAQ==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.34.0"
|
||||
"@typescript-eslint/types" "5.34.0"
|
||||
"@typescript-eslint/typescript-estree" "5.34.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.34.0":
|
||||
version "5.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.34.0.tgz#d0fb3e31033e82ddd5de048371ad39eb342b2d40"
|
||||
integrity sha512-O1moYjOSrab0a2fUvFpsJe0QHtvTC+cR+ovYpgKrAVXzqQyc74mv76TgY6z+aEtjQE2vgZux3CQVtGryqdcOAw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.34.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@vue/eslint-config-prettier@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz#44ab55ca22401102b57795c59428e9dade72be34"
|
||||
integrity sha512-/CTc6ML3Wta1tCe1gUeO0EYnVXfo3nJXsIhZ8WJr3sov+cGASr6yuiibJTL6lmIBm7GobopToOuB3B6AWyV0Iw==
|
||||
dependencies:
|
||||
eslint-config-prettier "^8.3.0"
|
||||
eslint-plugin-prettier "^4.0.0"
|
||||
|
||||
"@vue/eslint-config-typescript@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.0.tgz#bac0cb2d381625b5bf568d2025acffc0fd09113e"
|
||||
integrity sha512-txuRzxnQVmtUvvy9UyWUy9sHWXNeRPGmSPqP53hRtaiUeCTAondI9Ho9GQYI/8/eWljYOST7iA4Aa8sANBkWaA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "^5.0.0"
|
||||
"@typescript-eslint/parser" "^5.0.0"
|
||||
vue-eslint-parser "^9.0.0"
|
||||
|
||||
"@yarnpkg/lockfile@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||
|
@ -1050,11 +1191,21 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
|
|||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^7.1.1:
|
||||
version "7.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.8.0:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
||||
|
||||
admin-config@^0.12.4:
|
||||
version "0.12.4"
|
||||
resolved "https://registry.yarnpkg.com/admin-config/-/admin-config-0.12.4.tgz#b0a324785c0e5b8ec489148ac6d1402ae851dac3"
|
||||
|
@ -1084,7 +1235,7 @@ aggregate-error@^3.0.0:
|
|||
clean-stack "^2.0.0"
|
||||
indent-string "^4.0.0"
|
||||
|
||||
ajv@^6.12.3:
|
||||
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
|
@ -2110,7 +2261,7 @@ cross-spawn@^5.1.0:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
|
@ -2178,7 +2329,7 @@ debug@2.6.9, debug@^2.1.3:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3:
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
|
@ -2207,7 +2358,7 @@ deep-extend@^0.6.0, deep-extend@~0.6.0:
|
|||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@~0.1.3:
|
||||
deep-is@^0.1.3, deep-is@~0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
@ -2274,6 +2425,13 @@ discontinuous-range@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
|
||||
integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
doctypes@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
||||
|
@ -2473,6 +2631,11 @@ escape-string-regexp@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
escape-string-regexp@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
|
@ -2490,6 +2653,105 @@ escodegen@1.8.x:
|
|||
optionalDependencies:
|
||||
source-map "~0.2.0"
|
||||
|
||||
eslint-config-prettier@^8.3.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
|
||||
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
|
||||
|
||||
eslint-plugin-prettier@^4.0.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
|
||||
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-scope@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
|
||||
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
|
||||
integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-utils@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
|
||||
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
eslint-visitor-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
|
||||
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@^8.22.0:
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.22.0.tgz#78fcb044196dfa7eef30a9d65944f6f980402c48"
|
||||
integrity sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.0"
|
||||
"@humanwhocodes/config-array" "^0.10.4"
|
||||
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
|
||||
ajv "^6.10.0"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
debug "^4.3.2"
|
||||
doctrine "^3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^7.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
espree "^9.3.3"
|
||||
esquery "^1.4.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
find-up "^5.0.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob-parent "^6.0.1"
|
||||
globals "^13.15.0"
|
||||
globby "^11.1.0"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.1.2"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.1"
|
||||
regexpp "^3.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
strip-json-comments "^3.1.0"
|
||||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
espree@^9.3.1, espree@^9.3.2, espree@^9.3.3:
|
||||
version "9.3.3"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d"
|
||||
integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==
|
||||
dependencies:
|
||||
acorn "^8.8.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
esprima@2.7.x, esprima@^2.7.1:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
|
||||
|
@ -2500,11 +2762,35 @@ esprima@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
|
||||
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
|
||||
dependencies:
|
||||
estraverse "^5.2.0"
|
||||
|
||||
estraverse@^1.9.1:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
|
||||
integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=
|
||||
|
||||
estraverse@^4.1.1:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.1.0, estraverse@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
|
@ -2627,12 +2913,17 @@ extsprintf@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
|
||||
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@^3.2.11:
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-glob@^3.2.11, fast-glob@^3.2.9:
|
||||
version "3.2.11"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
|
||||
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
|
||||
|
@ -2648,10 +2939,10 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
fast-levenshtein@~2.0.6:
|
||||
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.13.0"
|
||||
|
@ -2689,6 +2980,13 @@ figures@^3.2.0:
|
|||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-sync-cmp@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b"
|
||||
|
@ -2766,6 +3064,14 @@ find-up@^4.0.0, find-up@^4.1.0:
|
|||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
find-up@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
|
||||
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
|
||||
dependencies:
|
||||
locate-path "^6.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
find-up@^6.1.0, find-up@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790"
|
||||
|
@ -2817,6 +3123,19 @@ flagged-respawn@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
|
||||
integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||
integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
|
||||
dependencies:
|
||||
flatted "^3.1.0"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
flatted@^3.1.0:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
|
||||
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
|
||||
|
@ -2903,6 +3222,11 @@ function-bind@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
|
||||
|
||||
gauge@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
||||
|
@ -2992,6 +3316,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
|
|||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob-parent@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
|
||||
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^5.0.15, glob@~5.0.0:
|
||||
version "5.0.15"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||
|
@ -3063,6 +3394,25 @@ globals@^11.1.0:
|
|||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globals@^13.15.0:
|
||||
version "13.17.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
|
||||
integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
globby@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
|
||||
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
|
||||
dependencies:
|
||||
array-union "^2.1.0"
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.2.9"
|
||||
ignore "^5.2.0"
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
globby@^13.1.2:
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
|
||||
|
@ -3088,6 +3438,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0,
|
|||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
grapheme-splitter@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
grunt-cli@^1.3.2, grunt-cli@^1.4.3, grunt-cli@~1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.4.3.tgz#22c9f1a3d2780bf9b0d206e832e40f8f499175ff"
|
||||
|
@ -3636,6 +3991,14 @@ ignore@^5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
|
||||
|
||||
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-local@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
|
||||
|
@ -3784,7 +4147,7 @@ is-generator-fn@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
|
||||
integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
|
@ -4399,6 +4762,11 @@ json-schema@0.4.0:
|
|||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
|
@ -4459,6 +4827,14 @@ leven@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
levn@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||
|
@ -4527,6 +4903,13 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
locate-path@^7.1.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.1.1.tgz#8e1e5a75c7343770cef02ff93c4bf1f0aa666374"
|
||||
|
@ -4559,6 +4942,11 @@ lodash.memoize@4.x:
|
|||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.times@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.times/-/lodash.times-4.3.2.tgz#3e1f2565c431754d54ab57f2ed1741939285ca1d"
|
||||
|
@ -4753,7 +5141,7 @@ mimic-fn@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.1:
|
||||
"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
|
@ -5277,6 +5665,18 @@ optionator@^0.8.1:
|
|||
type-check "~0.3.2"
|
||||
word-wrap "~1.2.3"
|
||||
|
||||
optionator@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
|
||||
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
|
||||
dependencies:
|
||||
deep-is "^0.1.3"
|
||||
fast-levenshtein "^2.0.6"
|
||||
levn "^0.4.1"
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.3"
|
||||
|
||||
os-browserify@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54"
|
||||
|
@ -5307,7 +5707,7 @@ p-limit@^2.0.0, p-limit@^2.2.0:
|
|||
dependencies:
|
||||
p-try "^2.0.0"
|
||||
|
||||
p-limit@^3.1.0:
|
||||
p-limit@^3.0.2, p-limit@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
|
||||
|
@ -5335,6 +5735,13 @@ p-locate@^4.1.0:
|
|||
dependencies:
|
||||
p-limit "^2.2.0"
|
||||
|
||||
p-locate@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
|
||||
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
|
||||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
p-locate@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f"
|
||||
|
@ -5380,6 +5787,13 @@ param-case@^2.1.1:
|
|||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-filepath@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
|
||||
|
@ -5595,11 +6009,28 @@ portscanner@^2.2.0:
|
|||
async "^2.6.0"
|
||||
is-number-like "^1.0.3"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prelude-ls@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||
|
||||
pretty-bytes@^5.1.0, pretty-bytes@^5.3.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
|
@ -5994,6 +6425,11 @@ reflect-metadata@0.1.12:
|
|||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
|
||||
integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
|
||||
|
||||
regexpp@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
|
||||
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
||||
|
||||
relateurl@^0.2.7:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
|
@ -6062,6 +6498,11 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1:
|
|||
expand-tilde "^2.0.0"
|
||||
global-modules "^1.0.0"
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve-from@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
|
||||
|
@ -6185,7 +6626,7 @@ semver-truncate@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@7.x, semver@^7.3.5, semver@^7.3.7:
|
||||
semver@7.x, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
|
||||
version "7.3.7"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
||||
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
|
||||
|
@ -6598,7 +7039,7 @@ strip-json-comments@1.0.x:
|
|||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
|
||||
integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
@ -6831,11 +7272,23 @@ ts-jest@^28.0.8:
|
|||
semver "7.x"
|
||||
yargs-parser "^21.0.1"
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
@ -6853,6 +7306,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
type-check@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||
|
@ -6865,6 +7325,11 @@ type-detect@4.0.8:
|
|||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
type-fest@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
type-fest@^0.21.3:
|
||||
version "0.21.3"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
||||
|
@ -7034,6 +7499,11 @@ uuid@^3.3.2:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||
|
||||
v8-to-istanbul@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4"
|
||||
|
@ -7084,6 +7554,19 @@ void-elements@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
|
||||
|
||||
vue-eslint-parser@^9.0.0:
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.0.3.tgz#0c17a89e0932cc94fa6a79f0726697e13bfe3c96"
|
||||
integrity sha512-yL+ZDb+9T0ELG4VIFo/2anAOz8SvBdlqEnQnvJ3M7Scq56DvtjY0VY88bByRZB0D4J0u8olBcfrXTVONXsh4og==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
eslint-scope "^7.1.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
espree "^9.3.1"
|
||||
esquery "^1.4.0"
|
||||
lodash "^4.17.21"
|
||||
semver "^7.3.6"
|
||||
|
||||
walker@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
|
||||
|
@ -7182,7 +7665,7 @@ with@^7.0.0:
|
|||
assert-never "^1.2.1"
|
||||
babel-walk "3.0.0-canary-5"
|
||||
|
||||
word-wrap@~1.2.3:
|
||||
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
|
|
Loading…
Reference in a new issue