diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..a3faa74 --- /dev/null +++ b/.eslintrc.cjs @@ -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", + ], +}; diff --git a/package.json b/package.json index 2b2efab..f49eb04 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/@types/http-auth-connect/index.d.ts b/server/@types/http-auth-connect/index.d.ts index 97be9b1..ee637b9 100644 --- a/server/@types/http-auth-connect/index.d.ts +++ b/server/@types/http-auth-connect/index.d.ts @@ -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; } diff --git a/server/@types/http-auth/index.d.ts b/server/@types/http-auth/index.d.ts index f2560c7..f4bcd59 100644 --- a/server/@types/http-auth/index.d.ts +++ b/server/@types/http-auth/index.d.ts @@ -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; } diff --git a/server/@types/nodemailer-html-to-text/index.d.ts b/server/@types/nodemailer-html-to-text/index.d.ts index 8eae196..3189727 100644 --- a/server/@types/nodemailer-html-to-text/index.d.ts +++ b/server/@types/nodemailer-html-to-text/index.d.ts @@ -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; } diff --git a/server/__mocks__/logger.test.ts b/server/__mocks__/logger.test.ts index 667d4bb..c576665 100644 --- a/server/__mocks__/logger.test.ts +++ b/server/__mocks__/logger.test.ts @@ -1,4 +1,4 @@ -import {MockLogger} from "./logger"; +import { MockLogger } from "./logger"; test("should reset single message", () => { // given @@ -104,7 +104,7 @@ test("should get messages for no tag", () => { // when logger.tag().debug("message"); - + // then expect(logger.getMessages("debug")).toEqual([["message"]]); }); @@ -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, {}], + ]); }); diff --git a/server/__mocks__/logger.ts b/server/__mocks__/logger.ts index 63b7a63..418620c 100644 --- a/server/__mocks__/logger.ts +++ b/server/__mocks__/logger.ts @@ -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); }, - } + }; } } diff --git a/server/app.ts b/server/app.ts index fa0dba3..2785bc6 100644 --- a/server/app.ts +++ b/server/app.ts @@ -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(a: T, b: T): boolean { return different === 0; } -async function isValidLogin(username: Username, password: CleartextPassword): Promise { +async function isValidLogin( + username: Username, + password: CleartextPassword +): Promise { 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 { + function usePromise( + f: (req: Request, res: Response) => Promise + ): 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 { - const body = await fs.readFile(templateDir + '/' + req.path + '.template', 'utf8'); + async function serveTemplate( + mimeType: string, + req: Request, + res: Response + ): Promise { + 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 => { 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); } diff --git a/server/config.ts b/server/config.ts index a835a47..d494df5 100644 --- a/server/config.ts +++ b/server/config.ts @@ -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; } diff --git a/server/db/__mocks__/database.ts b/server/db/__mocks__/database.ts index 0becad7..769b188 100644 --- a/server/db/__mocks__/database.ts +++ b/server/db/__mocks__/database.ts @@ -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 { + 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 { + return; } - async on(event: string, listener: any): Promise { - } - - async run(sql: SqlType, ...params: any[]): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async run(sql: SqlType, ...params: unknown[]): Promise { return { stmt: new Statement(new sqlite3.Statement()), }; } - async get(sql: SqlType, ...params: any[]): Promise { + async get( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sql: SqlType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ...params: unknown[] + ): Promise { return undefined; } - async each(sql: SqlType, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, ...params: any[]): Promise; - async each(sql: SqlType, ...callback: (any)[]): Promise { + async each( + sql: SqlType, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + param2: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + param2: unknown, + param3: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async each( + sql: SqlType, + ...params: unknown[] + ): Promise; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async each(sql: SqlType, ...callback: unknown[]): Promise { return 0; } - async all(sql: SqlType, ...params: any[]): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async all(sql: SqlType, ...params: unknown[]): Promise { return []; } - async exec(sql: SqlType, ...params: any[]): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async exec(sql: SqlType, ...params: unknown[]): Promise { + return; } - - async prepare(sql: SqlType, ...params: any[]): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async prepare(sql: SqlType, ...params: unknown[]): Promise { return new Statement(new sqlite3.Statement()); } } export const db: MockDatabase = new MockDatabase(); -export {TypedDatabase, Statement} +export { TypedDatabase, Statement }; diff --git a/server/db/database.ts b/server/db/database.ts index 5d64028..3a96105 100644 --- a/server/db/database.ts +++ b/server/db/database.ts @@ -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 { + async on(event: string, listener: unknown): Promise { const db = await this.db; db.on(event, listener); } - async run(sql: SqlType, ...params: any[]): Promise { + async run(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; return db.run(sql, ...params); } - async get(sql: SqlType, ...params: any[]): Promise { + async get(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; return await db.get(sql, ...params); } - async each(sql: SqlType, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise; - async each(sql: SqlType, ...params: any[]): Promise { + async each( + sql: SqlType, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + param2: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + async each( + sql: SqlType, + param1: unknown, + param2: unknown, + param3: unknown, + callback: (err: unknown, row: T) => void + ): Promise; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async each(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; - // @ts-ignore - return await db.each.apply(db, arguments); + return await db.each(sql, ...params); } - async all(sql: SqlType, ...params: any[]): Promise { + async all(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; - return (await db.all(sql, ...params)); + return await db.all(sql, ...params); } - async exec(sql: SqlType, ...params: any[]): Promise { + async exec(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; return await db.exec(sql, ...params); } - async prepare(sql: SqlType, ...params: any[]): Promise { + async prepare(sql: SqlType, ...params: unknown[]): Promise { const db = await this.db; return await db.prepare(sql, ...params); } } async function applyPatch(db: TypedDatabase, file: string): Promise { - 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 { - 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 { - 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; } } diff --git a/server/init.js b/server/init.js deleted file mode 100644 index 51dec94..0000000 --- a/server/init.js +++ /dev/null @@ -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(); -})(); diff --git a/server/init.ts b/server/init.ts new file mode 100644 index 0000000..928677b --- /dev/null +++ b/server/init.ts @@ -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(); diff --git a/server/jobs/FixNodeFilenamesJob.ts b/server/jobs/FixNodeFilenamesJob.ts index ce5e4c1..245212b 100644 --- a/server/jobs/FixNodeFilenamesJob.ts +++ b/server/jobs/FixNodeFilenamesJob.ts @@ -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(); }, -} +}; diff --git a/server/jobs/MailQueueJob.ts b/server/jobs/MailQueueJob.ts index f7405aa..b82aec8 100644 --- a/server/jobs/MailQueueJob.ts +++ b/server/jobs/MailQueueJob.ts @@ -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(); diff --git a/server/jobs/MonitoringMailsSendingJob.ts b/server/jobs/MonitoringMailsSendingJob.ts index 02720e6..7056892 100644 --- a/server/jobs/MonitoringMailsSendingJob.ts +++ b/server/jobs/MonitoringMailsSendingJob.ts @@ -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(); diff --git a/server/jobs/NodeInformationRetrievalJob.ts b/server/jobs/NodeInformationRetrievalJob.ts index 5e65661..67482e0 100644 --- a/server/jobs/NodeInformationRetrievalJob.ts +++ b/server/jobs/NodeInformationRetrievalJob.ts @@ -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( diff --git a/server/jobs/OfflineNodesDeletionJob.ts b/server/jobs/OfflineNodesDeletionJob.ts index 9b39d89..2cda68f 100644 --- a/server/jobs/OfflineNodesDeletionJob.ts +++ b/server/jobs/OfflineNodesDeletionJob.ts @@ -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(); diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts index 0fafb5c..76a3672 100644 --- a/server/jobs/scheduler.ts +++ b/server/jobs/scheduler.ts @@ -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, + run(): Promise; } 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 { diff --git a/server/logger.test.ts b/server/logger.test.ts index 7103807..59c4093 100644 --- a/server/logger.test.ts +++ b/server/logger.test.ts @@ -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(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: [], + }, + ]); }); diff --git a/server/logger.ts b/server/logger.ts index d3dee8e..22daa9a 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -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; } diff --git a/server/mail/index.ts b/server/mail/index.ts index f47fec5..3bf343b 100644 --- a/server/mail/index.ts +++ b/server/mail/index.ts @@ -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"; diff --git a/server/main.ts b/server/main.ts index e5c2121..ee5a6d2 100755 --- a/server/main.ts +++ b/server/main.ts @@ -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); +}); diff --git a/server/resources/configResource.ts b/server/resources/configResource.ts index 43d9dfa..e4f3607 100644 --- a/server/resources/configResource.ts +++ b/server/resources/configResource.ts @@ -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); diff --git a/server/resources/frontendResource.ts b/server/resources/frontendResource.ts index a7e69c2..6fcd278 100644 --- a/server/resources/frontendResource.ts +++ b/server/resources/frontendResource.ts @@ -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( /window.__nodeToken = \''+ data.token + '\';window.__nodeToken = '" + + data.token + + "'; { - 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, + }); + }); } diff --git a/server/resources/mailResource.ts b/server/resources/mailResource.ts index 20069a5..143a648 100644 --- a/server/resources/mailResource.ts +++ b/server/resources/mailResource.ts @@ -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 { 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); }); diff --git a/server/resources/monitoringResource.ts b/server/resources/monitoringResource.ts index e4c34df..467b168 100644 --- a/server/resources/monitoringResource.ts +++ b/server/resources/monitoringResource.ts @@ -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(async data => { +export const confirm = handleJSONWithData(async (data) => { const validatedToken = getValidatedToken(data); const node = await MonitoringService.confirm(validatedToken); return toMonitoringResponse(node); }); -export const disable = handleJSONWithData(async data => { +export const disable = handleJSONWithData(async (data) => { const validatedToken: MonitoringToken = getValidatedToken(data); const node = await MonitoringService.disable(validatedToken); diff --git a/server/resources/nodeResource.ts b/server/resources/nodeResource.ts index 0df379a..45964c6 100644 --- a/server/resources/nodeResource.ts +++ b/server/resources/nodeResource.ts @@ -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(async data => { +export const create = handleJSONWithData(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(async data => { +export const update = handleJSONWithData(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(async data => { +export const remove = handleJSONWithData(async (data) => { const validatedToken = getValidatedToken(data); await NodeService.deleteNode(validatedToken); }); -export const get = handleJSONWithData(async data => { +export const get = handleJSONWithData(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( 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)); } diff --git a/server/resources/statisticsResource.ts b/server/resources/statisticsResource.ts index 6d32aa0..d7366b2 100644 --- a/server/resources/statisticsResource.ts +++ b/server/resources/statisticsResource.ts @@ -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 }; } }); diff --git a/server/resources/taskResource.ts b/server/resources/taskResource.ts index 079d2db..035d7bd 100644 --- a/server/resources/taskResource.ts +++ b/server/resources/taskResource.ts @@ -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 { 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 { 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 { return await getTask(id); } -async function setTaskEnabled(data: RequestData, enable: boolean): Promise { +async function setTaskEnabled( + data: RequestData, + enable: boolean +): Promise { 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); }); diff --git a/server/resources/versionResource.ts b/server/resources/versionResource.ts index 75a4c00..30e9681 100644 --- a/server/resources/versionResource.ts +++ b/server/resources/versionResource.ts @@ -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, })); diff --git a/server/router.ts b/server/router.ts index b1f0c4d..e930a36 100644 --- a/server/router.ts +++ b/server/router.ts @@ -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); } diff --git a/server/services/mailService.ts b/server/services/mailService.ts index d698902..f45f535 100644 --- a/server/services/mailService.ts +++ b/server/services/mailService.ts @@ -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 { - 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 { 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 { +async function findPendingMailsBefore( + beforeMoment: Moment, + limit: number +): Promise { const rows = await db.all( - '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 { - 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 { +async function incrementFailureCounterForPendingEmail( + id: MailId +): Promise { 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 { 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 { } async function doGetMail(id: MailId): Promise { - const row = await db.get('SELECT * FROM email_queue WHERE id = ?', [id]); + const row = await db.get("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 { +export async function enqueue( + sender: string, + recipient: string, + email: MailType, + data: MailData +): Promise { 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 { 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 { @@ -164,34 +180,39 @@ export async function deleteMail(id: MailId): Promise { export async function resetFailures(id: MailId): Promise { 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 { - 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."); } diff --git a/server/services/mailTemplateService.ts b/server/services/mailTemplateService.ts index d2289c4..9605115 100644 --- a/server/services/mailTemplateService.ts +++ b/server/services/mailTemplateService.ts @@ -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 { '<%- text %>' )({ 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; } } diff --git a/server/services/monitoringService.test.ts b/server/services/monitoringService.test.ts index 81769f4..cbb8e59 100644 --- a/server/services/monitoringService.test.ts +++ b/server/services/monitoringService.test.ts @@ -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); }); diff --git a/server/services/monitoringService.ts b/server/services/monitoringService.ts index dd99f6b..ef6bb56 100644 --- a/server/services/monitoringService.ts +++ b/server/services/monitoringService.ts @@ -1,8 +1,8 @@ import _ from "lodash"; import request from "request"; -import {config} from "../config"; -import {db} from "../db/database"; +import { config } from "../config"; +import { db } from "../db/database"; import * as DatabaseUtil from "../utils/databaseUtil"; import ErrorTypes from "../utils/errorTypes"; import Logger from "../logger"; @@ -10,22 +10,25 @@ import Logger from "../logger"; import * as MailService from "../services/mailService"; import * as NodeService from "../services/nodeService"; import * as Resources from "../utils/resources"; -import {RestParams} from "../utils/resources"; -import {normalizeMac, parseInteger} from "../shared/utils/strings"; -import {monitoringDisableUrl} from "../utils/urlBuilder"; +import { RestParams } from "../utils/resources"; +import { normalizeMac, parseInteger } from "../shared/utils/strings"; +import { monitoringDisableUrl } from "../utils/urlBuilder"; import CONSTRAINTS from "../shared/validation/constraints"; -import {forConstraint} from "../shared/validation/validator"; +import { forConstraint } from "../shared/validation/validator"; import { Domain, DurationSeconds, + filterUndefinedFromJSON, Hostname, isBoolean, isDomain, isMonitoringSortField, isOnlineState, + isPlainObject, isSite, isString, isUndefined, + JSONValue, MAC, MailType, MonitoringSortField, @@ -34,28 +37,37 @@ import { NodeId, NodeStateData, OnlineState, + parseJSON, RunResult, Site, StoredNode, toCreateOrUpdateNode, - UnixTimestampSeconds + UnixTimestampSeconds, } from "../types"; -import {days, formatTimestamp, hours, now, parseTimestamp, subtract, weeks} from "../utils/time"; +import { + days, + formatTimestamp, + hours, + now, + parseTimestamp, + subtract, + weeks, +} from "../utils/time"; type NodeStateRow = { - id: number, - created_at: UnixTimestampSeconds, - domain: Domain | null, - hostname: Hostname | null, - import_timestamp: UnixTimestampSeconds, - last_seen: UnixTimestampSeconds, - last_status_mail_sent: string | null, - last_status_mail_type: string | null, - mac: MAC, - modified_at: UnixTimestampSeconds, - monitoring_state: string | null, - site: Site | null, - state: string, + id: number; + created_at: UnixTimestampSeconds; + domain: Domain | null; + hostname: Hostname | null; + import_timestamp: UnixTimestampSeconds; + last_seen: UnixTimestampSeconds; + last_status_mail_sent: string | null; + last_status_mail_type: string | null; + mac: MAC; + modified_at: UnixTimestampSeconds; + monitoring_state: string | null; + site: Site | null; + state: string; }; const MONITORING_STATE_MACS_CHUNK_SIZE = 100; @@ -72,37 +84,41 @@ const MONITORING_OFFLINE_MAILS_SCHEDULE: Record = { const DELETE_OFFLINE_NODES_AFTER_DURATION: DurationSeconds = days(100); export type ParsedNode = { - mac: MAC, - importTimestamp: UnixTimestampSeconds, - state: OnlineState, - lastSeen: UnixTimestampSeconds, - site?: Site, - domain?: Domain, + mac: MAC; + importTimestamp: UnixTimestampSeconds; + state: OnlineState; + lastSeen: UnixTimestampSeconds; + site?: Site; + domain?: Domain; }; export type NodesParsingResult = { - importTimestamp: UnixTimestampSeconds, - nodes: ParsedNode[], - failedNodesCount: number, - totalNodesCount: number, -} + importTimestamp: UnixTimestampSeconds; + nodes: ParsedNode[]; + failedNodesCount: number; + totalNodesCount: number; +}; export type RetrieveNodeInformationResult = { - failedParsingNodesCount: number, - totalNodesCount: number, + failedParsingNodesCount: number; + totalNodesCount: number; }; let previousImportTimestamp: UnixTimestampSeconds | null = null; -async function insertNodeInformation(nodeData: ParsedNode, node: StoredNode): Promise { - Logger - .tag('monitoring', 'information-retrieval') - .debug('Node is new in monitoring, creating data: %s', nodeData.mac); +async function insertNodeInformation( + nodeData: ParsedNode, + node: StoredNode +): Promise { + Logger.tag("monitoring", "information-retrieval").debug( + "Node is new in monitoring, creating data: %s", + nodeData.mac + ); await db.run( - 'INSERT INTO node_state ' + - '(hostname, mac, site, domain, monitoring_state, state, last_seen, import_timestamp, last_status_mail_sent, last_status_mail_type) ' + - 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + "INSERT INTO node_state " + + "(hostname, mac, site, domain, monitoring_state, state, last_seen, import_timestamp, last_status_mail_sent, last_status_mail_type) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ node.hostname, node.mac, @@ -113,39 +129,46 @@ async function insertNodeInformation(nodeData: ParsedNode, node: StoredNode): Pr nodeData.lastSeen, nodeData.importTimestamp, null, // new node so we haven't send a mail yet - null // new node so we haven't send a mail yet + null, // new node so we haven't send a mail yet ] ); } -async function updateNodeInformation(nodeData: ParsedNode, node: StoredNode, row: any): Promise { - Logger - .tag('monitoring', 'informacallbacktion-retrieval') - .debug('Node is known in monitoring: %s', nodeData.mac); +async function updateNodeInformation( + nodeData: ParsedNode, + node: StoredNode, + row: any +): Promise { + Logger.tag("monitoring", "informacallbacktion-retrieval").debug( + "Node is known in monitoring: %s", + nodeData.mac + ); if (row.import_timestamp >= nodeData.importTimestamp) { - Logger - .tag('monitoring', 'information-retrieval') - .debug('No new data for node, skipping: %s', nodeData.mac); + Logger.tag("monitoring", "information-retrieval").debug( + "No new data for node, skipping: %s", + nodeData.mac + ); return; } - Logger - .tag('monitoring', 'information-retrieval') - .debug('New data for node, updating: %s', nodeData.mac); + Logger.tag("monitoring", "information-retrieval").debug( + "New data for node, updating: %s", + nodeData.mac + ); await db.run( - 'UPDATE node_state ' + - 'SET ' + - 'hostname = ?, ' + - 'site = ?, ' + - 'domain = ?, ' + - 'monitoring_state = ?, ' + - 'state = ?, ' + - 'last_seen = ?, ' + - 'import_timestamp = ?, ' + - 'modified_at = ? ' + - 'WHERE id = ? AND mac = ?', + "UPDATE node_state " + + "SET " + + "hostname = ?, " + + "site = ?, " + + "domain = ?, " + + "monitoring_state = ?, " + + "state = ?, " + + "last_seen = ?, " + + "import_timestamp = ?, " + + "modified_at = ? " + + "WHERE id = ? AND mac = ?", [ node.hostname, nodeData.site || row.site, @@ -157,15 +180,23 @@ async function updateNodeInformation(nodeData: ParsedNode, node: StoredNode, row now(), row.id, - node.mac + node.mac, ] ); } -async function storeNodeInformation(nodeData: ParsedNode, node: StoredNode): Promise { - Logger.tag('monitoring', 'information-retrieval').debug('Storing status for node: %s', nodeData.mac); +async function storeNodeInformation( + nodeData: ParsedNode, + node: StoredNode +): Promise { + Logger.tag("monitoring", "information-retrieval").debug( + "Storing status for node: %s", + nodeData.mac + ); - const row = await db.get('SELECT * FROM node_state WHERE mac = ?', [node.mac]); + const row = await db.get("SELECT * FROM node_state WHERE mac = ?", [ + node.mac, + ]); if (isUndefined(row)) { return await insertNodeInformation(nodeData, node); @@ -176,16 +207,17 @@ async function storeNodeInformation(nodeData: ParsedNode, node: StoredNode): Pro const isValidMac = forConstraint(CONSTRAINTS.node.mac, false); -export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): ParsedNode { - if (!_.isPlainObject(nodeData)) { - throw new Error( - 'Unexpected node type: ' + (typeof nodeData) - ); +export function parseNode( + importTimestamp: UnixTimestampSeconds, + nodeData: JSONValue +): ParsedNode { + if (!isPlainObject(nodeData)) { + throw new Error("Unexpected node type: " + typeof nodeData); } - if (!_.isPlainObject(nodeData.nodeinfo)) { + if (!isPlainObject(nodeData.nodeinfo)) { throw new Error( - 'Unexpected nodeinfo type: ' + (typeof nodeData.nodeinfo) + "Unexpected nodeinfo type: " + typeof nodeData.nodeinfo ); } @@ -196,27 +228,36 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): ); } - if (!_.isPlainObject(nodeData.nodeinfo.network)) { + if (!isPlainObject(nodeData.nodeinfo.network)) { throw new Error( - 'Node ' + nodeId + ': Unexpected nodeinfo.network type: ' + (typeof nodeData.nodeinfo.network) + "Node " + + nodeId + + ": Unexpected nodeinfo.network type: " + + typeof nodeData.nodeinfo.network ); } if (!isValidMac(nodeData.nodeinfo.network.mac)) { throw new Error( - 'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac + "Node " + nodeId + ": Invalid MAC: " + nodeData.nodeinfo.network.mac ); } - const mac = normalizeMac(nodeData.nodeinfo.network.mac) as MAC; + const mac = normalizeMac(nodeData.nodeinfo.network.mac as MAC); - if (!_.isPlainObject(nodeData.flags)) { + if (!isPlainObject(nodeData.flags)) { throw new Error( - 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags) + "Node " + + nodeId + + ": Unexpected flags type: " + + typeof nodeData.flags ); } if (!isBoolean(nodeData.flags.online)) { throw new Error( - 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) + "Node " + + nodeId + + ": Unexpected flags.online type: " + + typeof nodeData.flags.online ); } const isOnline = nodeData.flags.online; @@ -224,17 +265,26 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): const lastSeen = parseTimestamp(nodeData.lastseen); if (lastSeen === null) { throw new Error( - 'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen + "Node " + + nodeId + + ": Invalid lastseen timestamp: " + + nodeData.lastseen ); } let site: Site | undefined; - if (_.isPlainObject(nodeData.nodeinfo.system) && isSite(nodeData.nodeinfo.system.site_code)) { + if ( + isPlainObject(nodeData.nodeinfo.system) && + isSite(nodeData.nodeinfo.system.site_code) + ) { site = nodeData.nodeinfo.system.site_code; } let domain: Domain | undefined; - if (_.isPlainObject(nodeData.nodeinfo.system) && isDomain(nodeData.nodeinfo.system.domain_code)) { + if ( + isPlainObject(nodeData.nodeinfo.system) && + isDomain(nodeData.nodeinfo.system.domain_code) + ) { domain = nodeData.nodeinfo.system.domain_code; } @@ -249,22 +299,28 @@ export function parseNode(importTimestamp: UnixTimestampSeconds, nodeData: any): } export function parseNodesJson(body: string): NodesParsingResult { - Logger.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...'); + Logger.tag("monitoring", "information-retrieval").debug( + "Parsing nodes.json..." + ); - const json = JSON.parse(body); + const json = parseJSON(body); - if (!_.isPlainObject(json)) { - throw new Error(`Expecting a JSON object as the nodes.json root, but got: ${typeof json}`); + if (!isPlainObject(json)) { + throw new Error( + `Expecting a JSON object as the nodes.json root, but got: ${typeof json}` + ); } const expectedVersion = 2; if (json.version !== expectedVersion) { - throw new Error(`Unexpected nodes.json version "${json.version}". Expected: "${expectedVersion}"`); + throw new Error( + `Unexpected nodes.json version "${json.version}". Expected: "${expectedVersion}"` + ); } const importTimestamp = parseTimestamp(json.timestamp); if (importTimestamp === null) { - throw new Error('Invalid timestamp: ' + json.timestamp); + throw new Error("Invalid timestamp: " + json.timestamp); } const result: NodesParsingResult = { @@ -275,206 +331,251 @@ export function parseNodesJson(body: string): NodesParsingResult { }; if (!_.isArray(json.nodes)) { - throw new Error('Invalid nodes array type: ' + (typeof json.nodes)); + throw new Error("Invalid nodes array type: " + typeof json.nodes); } for (const nodeData of json.nodes) { result.totalNodesCount += 1; try { const parsedNode = parseNode(result.importTimestamp, nodeData); - Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`); + Logger.tag("monitoring", "parsing-nodes-json").debug( + `Parsing node successful: ${parsedNode.mac}` + ); result.nodes.push(parsedNode); } catch (error) { result.failedNodesCount += 1; - Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData); + Logger.tag("monitoring", "parsing-nodes-json").error( + "Could not parse node.", + error, + nodeData + ); } } return result; } -async function updateSkippedNode(id: NodeId, node?: StoredNode): Promise { +async function updateSkippedNode( + id: NodeId, + node?: StoredNode +): Promise { return await db.run( - 'UPDATE node_state ' + - 'SET hostname = ?, monitoring_state = ?, modified_at = ?' + - 'WHERE id = ?', - [ - node ? node.hostname : '', node ? node.monitoringState : '', now(), - id - ] + "UPDATE node_state " + + "SET hostname = ?, monitoring_state = ?, modified_at = ?" + + "WHERE id = ?", + [node ? node.hostname : "", node ? node.monitoringState : "", now(), id] ); } async function sendMonitoringMailsBatched( name: string, mailType: MailType, - findBatchFun: () => Promise, + findBatchFun: () => Promise ): Promise { - Logger.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name); + Logger.tag("monitoring", "mail-sending").debug( + 'Sending "%s" mails...', + name + ); while (true) { - Logger.tag('monitoring', 'mail-sending').debug('Sending next batch...'); + Logger.tag("monitoring", "mail-sending").debug("Sending next batch..."); const nodeStates = await findBatchFun(); if (_.isEmpty(nodeStates)) { - Logger.tag('monitoring', 'mail-sending').debug('Done sending "%s" mails.', name); + Logger.tag("monitoring", "mail-sending").debug( + 'Done sending "%s" mails.', + name + ); return; } for (const nodeState of nodeStates) { const mac = nodeState.mac; - Logger.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac); + Logger.tag("monitoring", "mail-sending").debug( + "Loading node data for: %s", + mac + ); const result = await NodeService.findNodeDataWithSecretsByMac(mac); if (!result) { - Logger - .tag('monitoring', 'mail-sending') - .debug( - 'Node not found. Skipping sending of "' + name + '" mail: ' + mac - ); + Logger.tag("monitoring", "mail-sending").debug( + 'Node not found. Skipping sending of "' + + name + + '" mail: ' + + mac + ); await updateSkippedNode(nodeState.id); continue; } - const {node, nodeSecrets} = result; + const { node, nodeSecrets } = result; if (node.monitoringState !== MonitoringState.ACTIVE) { - Logger - .tag('monitoring', 'mail-sending') - .debug('Monitoring disabled, skipping "%s" mail for: %s', name, mac); + Logger.tag("monitoring", "mail-sending").debug( + 'Monitoring disabled, skipping "%s" mail for: %s', + name, + mac + ); await updateSkippedNode(nodeState.id, node); continue; } const monitoringToken = nodeSecrets.monitoringToken; if (!monitoringToken) { - Logger - .tag('monitoring', 'mail-sending') - .error('Node has no monitoring token. Cannot send mail "%s" for: %s', name, mac); + Logger.tag("monitoring", "mail-sending").error( + 'Node has no monitoring token. Cannot send mail "%s" for: %s', + name, + mac + ); await updateSkippedNode(nodeState.id, node); continue; } - Logger - .tag('monitoring', 'mail-sending') - .info('Sending "%s" mail for: %s', name, mac); + Logger.tag("monitoring", "mail-sending").info( + 'Sending "%s" mail for: %s', + name, + mac + ); await MailService.enqueue( config.server.email.from, - node.nickname + ' <' + node.email + '>', + node.nickname + " <" + node.email + ">", mailType, { - node: node, + node: filterUndefinedFromJSON(node), lastSeen: nodeState.last_seen, disableUrl: monitoringDisableUrl(monitoringToken), } ); - Logger - .tag('monitoring', 'mail-sending') - .debug('Updating node state: ', mac); + Logger.tag("monitoring", "mail-sending").debug( + "Updating node state: ", + mac + ); const timestamp = now(); await db.run( - 'UPDATE node_state ' + - 'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' + - 'WHERE id = ?', + "UPDATE node_state " + + "SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?" + + "WHERE id = ?", [ - node.hostname, node.monitoringState, timestamp, timestamp, mailType, - nodeState.id + node.hostname, + node.monitoringState, + timestamp, + timestamp, + mailType, + nodeState.id, ] ); } } } -async function sendOnlineAgainMails(startTime: UnixTimestampSeconds): Promise { +async function sendOnlineAgainMails( + startTime: UnixTimestampSeconds +): Promise { await sendMonitoringMailsBatched( - 'online again', + "online again", MailType.MONITORING_ONLINE_AGAIN, - async (): Promise => await db.all( - 'SELECT * FROM node_state ' + - 'WHERE modified_at < ? AND state = ? AND last_status_mail_type IN (' + - '\'monitoring-offline-1\', \'monitoring-offline-2\', \'monitoring-offline-3\'' + - ')' + - 'ORDER BY id ASC LIMIT ?', - [ - startTime, - 'ONLINE', - - MONITORING_MAILS_DB_BATCH_SIZE - ], - ), + async (): Promise => + await db.all( + "SELECT * FROM node_state " + + "WHERE modified_at < ? AND state = ? AND last_status_mail_type IN (" + + "'monitoring-offline-1', 'monitoring-offline-2', 'monitoring-offline-3'" + + ")" + + "ORDER BY id ASC LIMIT ?", + [startTime, "ONLINE", MONITORING_MAILS_DB_BATCH_SIZE] + ) ); } -async function sendOfflineMails(startTime: UnixTimestampSeconds, mailType: MailType): Promise { +async function sendOfflineMails( + startTime: UnixTimestampSeconds, + mailType: MailType +): Promise { const mailNumber = parseInteger(mailType.split("-")[2]); await sendMonitoringMailsBatched( - 'offline ' + mailNumber, + "offline " + mailNumber, mailType, async (): Promise => { const previousType = - mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1)); + mailNumber === 1 + ? "monitoring-online-again" + : "monitoring-offline-" + (mailNumber - 1); // the first time the first offline mail is send, there was no mail before - const allowNull = mailNumber === 1 ? ' OR last_status_mail_type IS NULL' : ''; + const allowNull = + mailNumber === 1 ? " OR last_status_mail_type IS NULL" : ""; const schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber]; const scheduledTimeBefore = subtract(now(), schedule); return await db.all( - 'SELECT * FROM node_state ' + - 'WHERE modified_at < ? AND state = ? AND (last_status_mail_type = ?' + allowNull + ') AND ' + - 'last_seen <= ? AND (last_status_mail_sent <= ? OR last_status_mail_sent IS NULL) ' + - 'ORDER BY id ASC LIMIT ?', + "SELECT * FROM node_state " + + "WHERE modified_at < ? AND state = ? AND (last_status_mail_type = ?" + + allowNull + + ") AND " + + "last_seen <= ? AND (last_status_mail_sent <= ? OR last_status_mail_sent IS NULL) " + + "ORDER BY id ASC LIMIT ?", [ startTime, - 'OFFLINE', + "OFFLINE", previousType, scheduledTimeBefore, scheduledTimeBefore, - MONITORING_MAILS_DB_BATCH_SIZE - ], + MONITORING_MAILS_DB_BATCH_SIZE, + ] ); - }, + } ); } -function doRequest(url: string): Promise<{ response: request.Response, body: string }> { - return new Promise<{ response: request.Response, body: string }>((resolve, reject) => { - request(url, function (err, response, body) { - if (err) { - return reject(err); - } +function doRequest( + url: string +): Promise<{ response: request.Response; body: string }> { + return new Promise<{ response: request.Response; body: string }>( + (resolve, reject) => { + request(url, function (err, response, body) { + if (err) { + return reject(err); + } - resolve({response, body}); - }); - }); + resolve({ response, body }); + }); + } + ); } async function withUrlsData(urls: string[]): Promise { const results: NodesParsingResult[] = []; for (const url of urls) { - Logger.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url); + Logger.tag("monitoring", "information-retrieval").debug( + "Retrieving nodes.json: %s", + url + ); - const {response, body} = await doRequest(url); + const { response, body } = await doRequest(url); if (response.statusCode !== 200) { throw new Error( - 'Could not download nodes.json from ' + url + ': ' + - response.statusCode + ' - ' + response.statusMessage + "Could not download nodes.json from " + + url + + ": " + + response.statusCode + + " - " + + response.statusMessage ); } results.push(await parseNodesJson(body)); - } return results; } -async function retrieveNodeInformationForUrls(urls: string[]): Promise { +async function retrieveNodeInformationForUrls( + urls: string[] +): Promise { const datas = await withUrlsData(urls); let maxTimestamp = datas[0].importTimestamp; @@ -495,14 +596,15 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise= previousImportTimestamp) { - Logger - .tag('monitoring', 'information-retrieval') - .debug( - 'No new data, skipping. Current timestamp: %s, previous timestamp: %s', - formatTimestamp(maxTimestamp), - formatTimestamp(previousImportTimestamp) - ); + if ( + previousImportTimestamp !== null && + maxTimestamp >= previousImportTimestamp + ) { + Logger.tag("monitoring", "information-retrieval").debug( + "No new data, skipping. Current timestamp: %s, previous timestamp: %s", + formatTimestamp(maxTimestamp), + formatTimestamp(previousImportTimestamp) + ); return { failedParsingNodesCount, totalNodesCount, @@ -512,68 +614,76 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise data.nodes); + const allNodes = _.flatMap(datas, (data) => data.nodes); // Get rid of duplicates from different nodes.json files. Always use the one with the newest - const sortedNodes = _.orderBy(allNodes, [node => node.lastSeen], ['desc']); + const sortedNodes = _.orderBy( + allNodes, + [(node) => node.lastSeen], + ["desc"] + ); const uniqueNodes = _.uniqBy(sortedNodes, function (node) { return node.mac; }); for (const nodeData of uniqueNodes) { - Logger.tag('monitoring', 'information-retrieval').debug('Importing: %s', nodeData.mac); + Logger.tag("monitoring", "information-retrieval").debug( + "Importing: %s", + nodeData.mac + ); const result = await NodeService.findNodeDataByMac(nodeData.mac); if (!result) { - Logger - .tag('monitoring', 'information-retrieval') - .debug('Unknown node, skipping: %s', nodeData.mac); + Logger.tag("monitoring", "information-retrieval").debug( + "Unknown node, skipping: %s", + nodeData.mac + ); continue; } await storeNodeInformation(nodeData, result); - Logger - .tag('monitoring', 'information-retrieval') - .debug('Updating / deleting node data done: %s', nodeData.mac); + Logger.tag("monitoring", "information-retrieval").debug( + "Updating / deleting node data done: %s", + nodeData.mac + ); } - Logger - .tag('monitoring', 'information-retrieval') - .debug('Marking missing nodes as offline.'); + Logger.tag("monitoring", "information-retrieval").debug( + "Marking missing nodes as offline." + ); // Mark nodes as offline that haven't been imported in this run. await db.run( - 'UPDATE node_state ' + - 'SET state = ?, modified_at = ?' + - 'WHERE import_timestamp < ?', - [ - OnlineState.OFFLINE, now(), - minTimestamp - ] + "UPDATE node_state " + + "SET state = ?, modified_at = ?" + + "WHERE import_timestamp < ?", + [OnlineState.OFFLINE, now(), minTimestamp] ); return { failedParsingNodesCount, totalNodesCount, - } + }; } // FIXME: Replace any[] by type. -export async function getAll(restParams: RestParams): Promise<{ total: number, monitoringStates: any[] }> { +export async function getAll( + restParams: RestParams +): Promise<{ total: number; monitoringStates: any[] }> { const filterFields = [ - 'hostname', - 'mac', - 'monitoring_state', - 'state', - 'last_status_mail_type' + "hostname", + "mac", + "monitoring_state", + "state", + "last_status_mail_type", ]; const where = Resources.whereCondition(restParams, filterFields); const row = await db.get<{ total: number }>( - 'SELECT count(*) AS total FROM node_state WHERE ' + where.query, - where.params, + "SELECT count(*) AS total FROM node_state WHERE " + where.query, + where.params ); const total = row?.total || 0; @@ -586,14 +696,16 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m ); const monitoringStates = await db.all( - 'SELECT * FROM node_state WHERE ' + filter.query, - filter.params, + "SELECT * FROM node_state WHERE " + filter.query, + filter.params ); - return {monitoringStates, total}; + return { monitoringStates, total }; } -export async function getByMacs(macs: MAC[]): Promise> { +export async function getByMacs( + macs: MAC[] +): Promise> { if (_.isEmpty(macs)) { return {}; } @@ -601,17 +713,19 @@ export async function getByMacs(macs: MAC[]): Promise const nodeStateByMac: { [key: string]: NodeStateData } = {}; for (const subMacs of _.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE)) { - const inCondition = DatabaseUtil.inCondition('mac', subMacs); + const inCondition = DatabaseUtil.inCondition("mac", subMacs); const rows = await db.all( - 'SELECT * FROM node_state WHERE ' + inCondition.query, - inCondition.params, + "SELECT * FROM node_state WHERE " + inCondition.query, + inCondition.params ); for (const row of rows) { const onlineState = row.state; if (!isOnlineState(onlineState)) { - throw new Error(`Invalid online state in database: "${onlineState}"`); + throw new Error( + `Invalid online state in database: "${onlineState}"` + ); } nodeStateByMac[row.mac] = { @@ -626,9 +740,14 @@ export async function getByMacs(macs: MAC[]): Promise } export async function confirm(token: MonitoringToken): Promise { - const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token); - if (node.monitoringState === MonitoringState.DISABLED || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { - throw {data: 'Invalid token.', type: ErrorTypes.badRequest}; + const { node, nodeSecrets } = + await NodeService.getNodeDataWithSecretsByMonitoringToken(token); + if ( + node.monitoringState === MonitoringState.DISABLED || + !nodeSecrets.monitoringToken || + nodeSecrets.monitoringToken !== token + ) { + throw { data: "Invalid token.", type: ErrorTypes.badRequest }; } if (node.monitoringState === MonitoringState.ACTIVE) { @@ -645,9 +764,14 @@ export async function confirm(token: MonitoringToken): Promise { } export async function disable(token: MonitoringToken): Promise { - const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token); - if (node.monitoringState === MonitoringState.DISABLED || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { - throw {data: 'Invalid token.', type: ErrorTypes.badRequest}; + const { node, nodeSecrets } = + await NodeService.getNodeDataWithSecretsByMonitoringToken(token); + if ( + node.monitoringState === MonitoringState.DISABLED || + !nodeSecrets.monitoringToken || + nodeSecrets.monitoringToken !== token + ) { + throw { data: "Invalid token.", type: ErrorTypes.badRequest }; } node.monitoringState = MonitoringState.DISABLED; @@ -664,14 +788,18 @@ export async function disable(token: MonitoringToken): Promise { export async function retrieveNodeInformation(): Promise { const urls = config.server.map.nodesJsonUrl; if (_.isEmpty(urls)) { - throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl') + throw new Error( + "No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl" + ); } return await retrieveNodeInformationForUrls(urls); } export async function sendMonitoringMails(): Promise { - Logger.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...'); + Logger.tag("monitoring", "mail-sending").debug( + "Sending monitoring mails..." + ); const startTime = now(); @@ -679,9 +807,10 @@ export async function sendMonitoringMails(): Promise { await sendOnlineAgainMails(startTime); } catch (error) { // only logging an continuing with next type - Logger - .tag('monitoring', 'mail-sending') - .error('Error sending "online again" mails.', error); + Logger.tag("monitoring", "mail-sending").error( + 'Error sending "online again" mails.', + error + ); } for (const mailType of [ @@ -693,97 +822,84 @@ export async function sendMonitoringMails(): Promise { await sendOfflineMails(startTime, mailType); } catch (error) { // only logging an continuing with next type - Logger - .tag('monitoring', 'mail-sending') - .error('Error sending "' + mailType + '" mails.', error); + Logger.tag("monitoring", "mail-sending").error( + 'Error sending "' + mailType + '" mails.', + error + ); } } } export async function deleteOfflineNodes(): Promise { - Logger - .tag('nodes', 'delete-offline') - .info( - `Deleting offline nodes older than ${DELETE_OFFLINE_NODES_AFTER_DURATION} seconds.` - ); + Logger.tag("nodes", "delete-offline").info( + `Deleting offline nodes older than ${DELETE_OFFLINE_NODES_AFTER_DURATION} seconds.` + ); - const deleteBefore = - subtract( - now(), - DELETE_OFFLINE_NODES_AFTER_DURATION, - ); + const deleteBefore = subtract(now(), DELETE_OFFLINE_NODES_AFTER_DURATION); await deleteNeverOnlineNodesBefore(deleteBefore); await deleteNodesOfflineSinceBefore(deleteBefore); } -async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds): Promise { - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Deleting nodes that were never online created before ' + - deleteBefore - ); +async function deleteNeverOnlineNodesBefore( + deleteBefore: UnixTimestampSeconds +): Promise { + Logger.tag("nodes", "delete-never-online").info( + "Deleting nodes that were never online created before " + deleteBefore + ); - const deletionCandidates: StoredNode[] = await NodeService.findNodesModifiedBefore(deleteBefore); + const deletionCandidates: StoredNode[] = + await NodeService.findNodesModifiedBefore(deleteBefore); - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Number of nodes created before ' + + Logger.tag("nodes", "delete-never-online").info( + "Number of nodes created before " + deleteBefore + - ': ' + + ": " + deletionCandidates.length - ); + ); - const deletionCandidateMacs: MAC[] = deletionCandidates.map(node => node.mac); - const chunks: MAC[][] = _.chunk(deletionCandidateMacs, NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE); + const deletionCandidateMacs: MAC[] = deletionCandidates.map( + (node) => node.mac + ); + const chunks: MAC[][] = _.chunk( + deletionCandidateMacs, + NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE + ); - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Number of chunks to check for deletion: ' + - chunks.length - ); + Logger.tag("nodes", "delete-never-online").info( + "Number of chunks to check for deletion: " + chunks.length + ); for (const macs of chunks) { - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Checking chunk of ' + - macs.length + - ' MACs for deletion.' - ); + Logger.tag("nodes", "delete-never-online").info( + "Checking chunk of " + macs.length + " MACs for deletion." + ); - const placeholders = macs.map(() => '?').join(','); + const placeholders = macs.map(() => "?").join(","); const rows: { mac: MAC }[] = await db.all( `SELECT * FROM node_state WHERE mac IN (${placeholders})`, macs ); - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Of the chunk of ' + + Logger.tag("nodes", "delete-never-online").info( + "Of the chunk of " + macs.length + - ' MACs there were ' + + " MACs there were " + rows.length + - ' nodes found in monitoring database. Those should be skipped.' - ); + " nodes found in monitoring database. Those should be skipped." + ); - const seenMacs: MAC[] = rows.map(row => row.mac); + const seenMacs: MAC[] = rows.map((row) => row.mac); const neverSeenMacs = _.difference(macs, seenMacs); - Logger - .tag('nodes', 'delete-never-online') - .info( - 'Of the chunk of ' + + Logger.tag("nodes", "delete-never-online").info( + "Of the chunk of " + macs.length + - ' MACs there are ' + + " MACs there are " + neverSeenMacs.length + - ' nodes that were never online. Those will be deleted.' - ); + " nodes that were never online. Those will be deleted." + ); for (const neverSeenMac of neverSeenMacs) { await deleteNodeByMac(neverSeenMac); @@ -791,13 +907,12 @@ async function deleteNeverOnlineNodesBefore(deleteBefore: UnixTimestampSeconds): } } -async function deleteNodesOfflineSinceBefore(deleteBefore: UnixTimestampSeconds): Promise { +async function deleteNodesOfflineSinceBefore( + deleteBefore: UnixTimestampSeconds +): Promise { const rows = await db.all( - 'SELECT * FROM node_state WHERE state = ? AND last_seen < ?', - [ - 'OFFLINE', - deleteBefore - ], + "SELECT * FROM node_state WHERE state = ? AND last_seen < ?", + ["OFFLINE", deleteBefore] ); for (const row of rows) { @@ -806,7 +921,7 @@ async function deleteNodesOfflineSinceBefore(deleteBefore: UnixTimestampSeconds) } async function deleteNodeByMac(mac: MAC): Promise { - Logger.tag('nodes', 'delete-offline').debug('Deleting node ' + mac); + Logger.tag("nodes", "delete-offline").debug("Deleting node " + mac); let node; @@ -814,7 +929,10 @@ async function deleteNodeByMac(mac: MAC): Promise { node = await NodeService.findNodeDataByMac(mac); } catch (error) { // Only log error. We try to delete the nodes state anyways. - Logger.tag('nodes', 'delete-offline').error('Could not find node to delete: ' + mac, error); + Logger.tag("nodes", "delete-offline").error( + "Could not find node to delete: " + mac, + error + ); } if (node && node.token) { @@ -822,13 +940,15 @@ async function deleteNodeByMac(mac: MAC): Promise { } try { - await db.run( - 'DELETE FROM node_state WHERE mac = ? AND state = ?', - [mac, 'OFFLINE'], - ); + await db.run("DELETE FROM node_state WHERE mac = ? AND state = ?", [ + mac, + "OFFLINE", + ]); } catch (error) { // Only log error and continue with next node. - Logger.tag('nodes', 'delete-offline').error('Could not delete node state: ' + mac, error); + Logger.tag("nodes", "delete-offline").error( + "Could not delete node state: " + mac, + error + ); } } - diff --git a/server/services/nodeService.ts b/server/services/nodeService.ts index ee83f6c..3e9c06e 100644 --- a/server/services/nodeService.ts +++ b/server/services/nodeService.ts @@ -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 { - 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 { @@ -97,24 +102,25 @@ function findNodeFilesSync(filter: NodeFilter) { } async function findFilesInPeersPath(): Promise { - 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(isT: TypeGuard, 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 { 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 { 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 { - 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 { +async function sendMonitoringConfirmationMail( + node: StoredNode, + nodeSecrets: NodeSecrets +): Promise { 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 { +export async function createNode( + node: CreateOrUpdateNode +): Promise { 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(); } - 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 return createdNode; } -export async function updateNode(token: Token, node: CreateOrUpdateNode): Promise { - const {node: currentNode, nodeSecrets} = await getNodeDataWithSecretsByToken(token); +export async function updateNode( + token: Token, + node: CreateOrUpdateNode +): Promise { + 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(); - } else { // email unchanged, keep token (fix if not set) and confirmation state monitoringState = currentNode.monitoringState; - monitoringToken = nodeSecrets.monitoringToken || generateToken(); + monitoringToken = + nodeSecrets.monitoringToken || + generateToken(); } 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 { return await writeNodeFile(true, token, node, monitoringState, nodeSecrets); } @@ -502,52 +613,58 @@ export async function getAllNodes(): Promise { 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 { - 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 { - 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 { - const {node} = await getNodeDataByFilePattern({monitoringToken: monitoringToken}); + const { node } = await getNodeDataByFilePattern({ + monitoringToken: monitoringToken, + }); return node; } @@ -555,7 +672,7 @@ export async function fixNodeFilenames(): Promise { 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 { 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 { +export async function findNodesModifiedBefore( + timestamp: UnixTimestampSeconds +): Promise { const nodes = await getAllNodes(); - return nodes.filter(node => node.modifiedAt < timestamp); + return nodes.filter((node) => node.modifiedAt < timestamp); } export async function getNodeStatistics(): Promise { @@ -584,8 +708,8 @@ export async function getNodeStatistics(): Promise { withCoords: 0, monitoring: { active: 0, - pending: 0 - } + pending: 0, + }, }; for (const node of nodes) { diff --git a/server/shared/types/index.ts b/server/shared/types/index.ts index 1df1b31..401be53 100644 --- a/server/shared/types/index.ts +++ b/server/shared/types/index.ts @@ -1,4 +1,4 @@ -import {ArrayField, Field, RawJsonField} from "sparkson"; +import { ArrayField, Field, RawJsonField } from "sparkson"; // Types shared with the client. export type TypeGuard = (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 { -} +export type JSONArray = Array; 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( + arg: unknown, + key: Key +): arg is Record { + 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(arg: unknown, isT: TypeGuard): arg is Array { 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 { +export function isMap(arg: unknown): arg is Map { 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, _example: Type): TypeGuard { + Value + // eslint-disable-next-line @typescript-eslint/no-unused-vars +>(isValue: TypeGuard, example: Type): TypeGuard { 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(isT: TypeGuard): TypeGuard { } export function toIsEnum(enumDef: E): EnumTypeGuard { - return (arg): arg is EnumValue => Object.values(enumDef).includes(arg as [keyof E]); + return (arg): arg is EnumValue => + 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(arg: unknown, isT: TypeGuard): arg is (T | undefined) { +export function isOptional( + arg: unknown, + isT: TypeGuard +): 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 & 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 { diff --git a/server/shared/utils/strings.ts b/server/shared/utils/strings.ts index b1fb991..69326ae 100644 --- a/server/shared/utils/strings.ts +++ b/server/shared/utils/strings.ts @@ -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}"` + ); } } diff --git a/server/shared/validation/constraints.ts b/server/shared/validation/constraints.ts index e6e85fd..d0d37dd 100644 --- a/server/shared/validation/constraints.ts +++ b/server/shared/validation/constraints.ts @@ -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; diff --git a/server/shared/validation/validator.ts b/server/shared/validation/validator.ts index 43983e3..02940cd 100644 --- a/server/shared/validation/validator.ts +++ b/server/shared/validation/validator.ts @@ -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); } diff --git a/server/types/config.ts b/server/types/config.ts index 98e0b61..2a1ad35 100644 --- a/server/types/config.ts +++ b/server/types/config.ts @@ -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 + ) {} } diff --git a/server/types/database.ts b/server/types/database.ts index a78d84e..ef3609b 100644 --- a/server/types/database.ts +++ b/server/types/database.ts @@ -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; + on(event: string, listener: unknown): Promise; /** * @see Database.run */ - run(sql: SqlType, ...params: any[]): Promise; + run(sql: SqlType, ...params: unknown[]): Promise; /** * @see Database.get */ - get(sql: SqlType, ...params: any[]): Promise; + get(sql: SqlType, ...params: unknown[]): Promise; /** * @see Database.each */ - each(sql: SqlType, callback: (err: any, row: T) => void): Promise; + each( + sql: SqlType, + callback: (err: unknown, row: T) => void + ): Promise; - each(sql: SqlType, param1: any, callback: (err: any, row: T) => void): Promise; + each( + sql: SqlType, + param1: unknown, + callback: (err: unknown, row: T) => void + ): Promise; - each(sql: SqlType, param1: any, param2: any, callback: (err: any, row: T) => void): Promise; + each( + sql: SqlType, + param1: unknown, + param2: unknown, + callback: (err: unknown, row: T) => void + ): Promise; - each(sql: SqlType, param1: any, param2: any, param3: any, callback: (err: any, row: T) => void): Promise; + each( + sql: SqlType, + param1: unknown, + param2: unknown, + param3: unknown, + callback: (err: unknown, row: T) => void + ): Promise; - each(sql: SqlType, ...params: any[]): Promise; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + each(sql: SqlType, ...params: unknown[]): Promise; /** * @see Database.all */ - all(sql: SqlType, ...params: any[]): Promise; + all(sql: SqlType, ...params: unknown[]): Promise; /** * @see Database.exec */ - exec(sql: SqlType, ...params: any[]): Promise; + exec(sql: SqlType, ...params: unknown[]): Promise; /** * @see Database.prepare */ - prepare(sql: SqlType, ...params: any[]): Promise; + prepare(sql: SqlType, ...params: unknown[]): Promise; } diff --git a/server/types/index.ts b/server/types/index.ts index f0d76c3..98375e0 100644 --- a/server/types/index.ts +++ b/server/types/index.ts @@ -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; -} +}; diff --git a/server/types/logger.ts b/server/types/logger.ts index 48811c4..3245bce 100644 --- a/server/types/logger.ts +++ b/server/types/logger.ts @@ -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 { diff --git a/server/utils/databaseUtil.ts b/server/utils/databaseUtil.ts index aad4b66..1e639e8 100644 --- a/server/utils/databaseUtil.ts +++ b/server/utils/databaseUtil.ts @@ -1,8 +1,16 @@ import _ from "lodash"; -export function inCondition(field: string, list: T[]): {query: string, params: T[]} { +export function inCondition( + 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, - } + }; } diff --git a/server/utils/errorTypes.js b/server/utils/errorTypes.js deleted file mode 100644 index f93e5e4..0000000 --- a/server/utils/errorTypes.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -module.exports = { - badRequest: {code: 400}, - notFound: {code: 404}, - conflict: {code: 409}, - internalError: {code: 500} -} diff --git a/server/utils/errorTypes.ts b/server/utils/errorTypes.ts new file mode 100644 index 0000000..63de38a --- /dev/null +++ b/server/utils/errorTypes.ts @@ -0,0 +1,6 @@ +export default { + badRequest: { code: 400 }, + notFound: { code: 404 }, + conflict: { code: 409 }, + internalError: { code: 500 }, +}; diff --git a/server/utils/resources.ts b/server/utils/resources.ts index cfeb065..c57e945 100644 --- a/server/utils/resources.ts +++ b/server/utils/resources.ts @@ -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( restParams: RestParams, defaultSortField: EnumValue, - isSortField: EnumTypeGuard, + isSortField: EnumTypeGuard ): OrderByClause { - let sortField: EnumValue | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined; + let sortField: EnumValue | 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 { - 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(entities: E[], allowedFilterFields: string[], restParams: RestParams): E[] { +export function filter( + 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(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(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(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, S extends string>(entities: T[], isSortField: TypeGuard, 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, + restParams: RestParams +): Type[] { + const sortField: SortField | undefined = isSortField(restParams._sortField) + ? restParams._sortField + : undefined; if (!sortField) { return entities; } @@ -259,69 +325,74 @@ export function sort, 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(entities: Entity[], restParams: RestParams): Entity[] { +export function getPageEntities( + 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( restParams: RestParams, defaultSortField: EnumValue, isSortField: EnumTypeGuard, - filterFields: string[], + filterFields: string[] ): FilterClause { - const orderBy = orderByClause( - restParams, - defaultSortField, - isSortField, - ); + const orderBy = orderByClause(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(handler: () => Promise): RequestHandler { +export function handleJSON( + handler: () => Promise +): 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(handler: (data: RequestData) => Promise): RequestHandler { +export function handleJSONWithData( + handler: (data: RequestData) => Promise +): 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)); }; } diff --git a/server/utils/time.test.ts b/server/utils/time.test.ts index b0334e9..d511eaa 100644 --- a/server/utils/time.test.ts +++ b/server/utils/time.test.ts @@ -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); }); diff --git a/server/utils/time.ts b/server/utils/time.ts index 83bcdf8..eaea7f5 100644 --- a/server/utils/time.ts +++ b/server/utils/time.ts @@ -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); } - diff --git a/server/utils/urlBuilder.ts b/server/utils/urlBuilder.ts index 384537c..e2888d6 100644 --- a/server/utils/urlBuilder.ts +++ b/server/utils/urlBuilder.ts @@ -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 }); } diff --git a/yarn.lock b/yarn.lock index ede44b4..acdc0d1 100644 --- a/yarn.lock +++ b/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==