Refactor some server-side string types into newtypes.
This commit is contained in:
parent
6c2bd85287
commit
720acfb276
|
@ -9,7 +9,7 @@ import {promises as fs} from "graceful-fs";
|
||||||
|
|
||||||
import {config} from "./config";
|
import {config} from "./config";
|
||||||
import type {CleartextPassword, PasswordHash, Username} from "./types";
|
import type {CleartextPassword, PasswordHash, Username} from "./types";
|
||||||
import {isString} from "./types";
|
import {isString, lift2, to} from "./types";
|
||||||
import Logger from "./logger";
|
import Logger from "./logger";
|
||||||
|
|
||||||
export const app: Express = express();
|
export const app: Express = express();
|
||||||
|
@ -17,7 +17,7 @@ export const app: Express = express();
|
||||||
/**
|
/**
|
||||||
* Used to have some password comparison in case the user does not exist to avoid timing attacks.
|
* Used to have some password comparison in case the user does not exist to avoid timing attacks.
|
||||||
*/
|
*/
|
||||||
const INVALID_PASSWORD_HASH = "$2b$05$JebmV1q/ySuxa89GoJYlc.6SEnj1OZYBOfTf.TYAehcC5HLeJiWPi";
|
const INVALID_PASSWORD_HASH: PasswordHash = to("$2b$05$JebmV1q/ySuxa89GoJYlc.6SEnj1OZYBOfTf.TYAehcC5HLeJiWPi");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trying to implement a timing safe string compare.
|
* Trying to implement a timing safe string compare.
|
||||||
|
@ -50,15 +50,15 @@ async function isValidLogin(username: Username, password: CleartextPassword): Pr
|
||||||
|
|
||||||
// Iterate over all users every time to reduce risk of timing attacks.
|
// Iterate over all users every time to reduce risk of timing attacks.
|
||||||
for (const userConfig of config.server.internal.users) {
|
for (const userConfig of config.server.internal.users) {
|
||||||
if (timingSafeEqual(username, userConfig.username)) {
|
if (lift2(timingSafeEqual)(username, userConfig.username)) {
|
||||||
passwordHash = userConfig.passwordHash;
|
passwordHash = userConfig.passwordHash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always compare some password even if the user does not exist to reduce risk of timing attacks.
|
// Always compare some password even if the user does not exist to reduce risk of timing attacks.
|
||||||
const isValidPassword = await bcrypt.compare(
|
const isValidPassword = await bcrypt.compare(
|
||||||
password,
|
password.value,
|
||||||
passwordHash || INVALID_PASSWORD_HASH
|
passwordHash?.value || INVALID_PASSWORD_HASH.value
|
||||||
);
|
);
|
||||||
|
|
||||||
// Make sure password is only considered valid is user exists and therefor passwordHash is not undefined.
|
// Make sure password is only considered valid is user exists and therefor passwordHash is not undefined.
|
||||||
|
@ -73,8 +73,8 @@ export function init(): void {
|
||||||
{
|
{
|
||||||
realm: 'Knotenformular - Intern'
|
realm: 'Knotenformular - Intern'
|
||||||
},
|
},
|
||||||
function (username: Username, password: CleartextPassword, callback: BasicAuthCheckerCallback): void {
|
function (username: string, password: string, callback: BasicAuthCheckerCallback): void {
|
||||||
isValidLogin(username, password)
|
isValidLogin(to(username), to(password))
|
||||||
.then(result => callback(result))
|
.then(result => callback(result))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
Logger.tag('login').error(err);
|
Logger.tag('login').error(err);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as Resources from "../utils/resources";
|
||||||
import {normalizeString} from "../utils/strings";
|
import {normalizeString} from "../utils/strings";
|
||||||
import {forConstraint} from "../validation/validator";
|
import {forConstraint} from "../validation/validator";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
|
import {MonitoringToken, to} from "../types";
|
||||||
|
|
||||||
const isValidToken = forConstraint(CONSTRAINTS.token, false);
|
const isValidToken = forConstraint(CONSTRAINTS.token, false);
|
||||||
|
|
||||||
|
@ -38,8 +39,9 @@ export function confirm(req: Request, res: Response): void {
|
||||||
if (!isValidToken(token)) {
|
if (!isValidToken(token)) {
|
||||||
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
const validatedToken: MonitoringToken = to(token);
|
||||||
|
|
||||||
MonitoringService.confirm(token)
|
MonitoringService.confirm(validatedToken)
|
||||||
.then(node => Resources.success(res, {
|
.then(node => Resources.success(res, {
|
||||||
hostname: node.hostname,
|
hostname: node.hostname,
|
||||||
mac: node.mac,
|
mac: node.mac,
|
||||||
|
@ -57,8 +59,9 @@ export function disable(req: Request, res: Response): void {
|
||||||
if (!isValidToken(token)) {
|
if (!isValidToken(token)) {
|
||||||
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
const validatedToken: MonitoringToken = to(token);
|
||||||
|
|
||||||
MonitoringService.disable(token)
|
MonitoringService.disable(validatedToken)
|
||||||
.then(node => Resources.success(res, {
|
.then(node => Resources.success(res, {
|
||||||
hostname: node.hostname,
|
hostname: node.hostname,
|
||||||
mac: node.mac,
|
mac: node.mac,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {forConstraint, forConstraints} from "../validation/validator";
|
||||||
import * as Resources from "../utils/resources";
|
import * as Resources from "../utils/resources";
|
||||||
import {Entity} from "../utils/resources";
|
import {Entity} from "../utils/resources";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import {EnhancedNode, isNodeSortField, Node} from "../types";
|
import {EnhancedNode, isNodeSortField, MAC, Node, to, Token} from "../types";
|
||||||
|
|
||||||
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
|
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
|
||||||
|
|
||||||
|
@ -49,13 +49,14 @@ export function update (req: Request, res: Response): void {
|
||||||
if (!isValidToken(token)) {
|
if (!isValidToken(token)) {
|
||||||
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
const validatedToken: Token = to(token);
|
||||||
|
|
||||||
const node = getNormalizedNodeData(data);
|
const node = getNormalizedNodeData(data);
|
||||||
if (!isValidNode(node)) {
|
if (!isValidNode(node)) {
|
||||||
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeService.updateNode(token, node)
|
NodeService.updateNode(validatedToken, node)
|
||||||
.then(result => Resources.success(res, result))
|
.then(result => Resources.success(res, result))
|
||||||
.catch(err => Resources.error(res, err));
|
.catch(err => Resources.error(res, err));
|
||||||
}
|
}
|
||||||
|
@ -67,8 +68,9 @@ export function remove(req: Request, res: Response): void {
|
||||||
if (!isValidToken(token)) {
|
if (!isValidToken(token)) {
|
||||||
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
const validatedToken: Token = to(token);
|
||||||
|
|
||||||
NodeService.deleteNode(token)
|
NodeService.deleteNode(validatedToken)
|
||||||
.then(() => Resources.success(res, {}))
|
.then(() => Resources.success(res, {}))
|
||||||
.catch(err => Resources.error(res, err));
|
.catch(err => Resources.error(res, err));
|
||||||
}
|
}
|
||||||
|
@ -78,8 +80,9 @@ export function get(req: Request, res: Response): void {
|
||||||
if (!isValidToken(token)) {
|
if (!isValidToken(token)) {
|
||||||
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
|
||||||
}
|
}
|
||||||
|
const validatedToken: Token = to(token);
|
||||||
|
|
||||||
NodeService.getNodeDataByToken(token)
|
NodeService.getNodeDataByToken(validatedToken)
|
||||||
.then(node => Resources.success(res, node))
|
.then(node => Resources.success(res, node))
|
||||||
.catch(err => Resources.error(res, err));
|
.catch(err => Resources.error(res, err));
|
||||||
}
|
}
|
||||||
|
@ -94,11 +97,11 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
|
||||||
!!node.token
|
!!node.token
|
||||||
);
|
);
|
||||||
|
|
||||||
const macs = _.map(realNodes, (node: Node): string => node.mac);
|
const macs: MAC[] = _.map(realNodes, (node: Node): MAC => node.mac);
|
||||||
const nodeStateByMac = await MonitoringService.getByMacs(macs);
|
const nodeStateByMac = await MonitoringService.getByMacs(macs);
|
||||||
|
|
||||||
const enhancedNodes: EnhancedNode[] = _.map(realNodes, (node: Node): EnhancedNode => {
|
const enhancedNodes: EnhancedNode[] = _.map(realNodes, (node: Node): EnhancedNode => {
|
||||||
const nodeState = nodeStateByMac[node.mac];
|
const nodeState = nodeStateByMac[node.mac.value];
|
||||||
if (nodeState) {
|
if (nodeState) {
|
||||||
return deepExtend({}, node, {
|
return deepExtend({}, node, {
|
||||||
site: nodeState.site,
|
site: nodeState.site,
|
||||||
|
|
|
@ -5,7 +5,6 @@ import * as Resources from "../utils/resources";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
|
|
||||||
export function get (req: Request, res: Response): void {
|
export function get (req: Request, res: Response): void {
|
||||||
// TODO: Promises and types.
|
|
||||||
getNodeStatistics()
|
getNodeStatistics()
|
||||||
.then(nodeStatistics => Resources.success(
|
.then(nodeStatistics => Resources.success(
|
||||||
res,
|
res,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {ParsedNode, parseNode, parseNodesJson, parseTimestamp} from "./monitoringService";
|
import {ParsedNode, parseNode, parseNodesJson, parseTimestamp} from "./monitoringService";
|
||||||
import {OnlineState} from "../types";
|
import {MAC, OnlineState, to} from "../types";
|
||||||
import Logger from '../logger';
|
import Logger from '../logger';
|
||||||
import {MockLogger} from "../__mocks__/logger";
|
import {MockLogger} from "../__mocks__/logger";
|
||||||
|
|
||||||
|
@ -240,12 +240,12 @@ test('parseNode() should succeed parsing node without site and domain', () => {
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: to("12:34:56:78:90:AB"),
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: OnlineState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: '<unknown-site>',
|
site: to("<unknown-site>"),
|
||||||
domain: '<unknown-domain>'
|
domain: to("<unknown-domain>"),
|
||||||
};
|
};
|
||||||
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
||||||
});
|
});
|
||||||
|
@ -272,12 +272,12 @@ test('parseNode() should succeed parsing node with site and domain', () => {
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: to("12:34:56:78:90:AB"),
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: OnlineState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: 'test-site',
|
site: to("test-site"),
|
||||||
domain: 'test-domain'
|
domain: to("test-domain")
|
||||||
};
|
};
|
||||||
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
expect(parseNode(importTimestamp, nodeData)).toEqual(expectedParsedNode);
|
||||||
});
|
});
|
||||||
|
@ -461,12 +461,12 @@ test('parseNodesJson() should parse valid nodes', () => {
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: to("12:34:56:78:90:AB"),
|
||||||
importTimestamp: parseTimestamp(TIMESTAMP_VALID_STRING),
|
importTimestamp: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
state: OnlineState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: 'test-site',
|
site: to("test-site"),
|
||||||
domain: 'test-domain'
|
domain: to("test-domain"),
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.importTimestamp.isValid()).toBe(true);
|
expect(result.importTimestamp.isValid()).toBe(true);
|
||||||
|
|
|
@ -17,14 +17,19 @@ import {monitoringDisableUrl} from "../utils/urlBuilder";
|
||||||
import CONSTRAINTS from "../validation/constraints";
|
import CONSTRAINTS from "../validation/constraints";
|
||||||
import {forConstraint} from "../validation/validator";
|
import {forConstraint} from "../validation/validator";
|
||||||
import {
|
import {
|
||||||
|
Domain,
|
||||||
|
equal,
|
||||||
isMonitoringSortField,
|
isMonitoringSortField,
|
||||||
MAC,
|
MAC,
|
||||||
MailType,
|
MailType,
|
||||||
MonitoringSortField,
|
MonitoringSortField,
|
||||||
|
MonitoringToken,
|
||||||
Node,
|
Node,
|
||||||
NodeId,
|
NodeId,
|
||||||
NodeStateData,
|
NodeStateData,
|
||||||
OnlineState,
|
OnlineState,
|
||||||
|
Site,
|
||||||
|
to,
|
||||||
UnixTimestampSeconds
|
UnixTimestampSeconds
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
@ -45,12 +50,12 @@ const DELETE_OFFLINE_NODES_AFTER_DURATION: { amount: number, unit: unitOfTime.Du
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedNode = {
|
export type ParsedNode = {
|
||||||
mac: string,
|
mac: MAC,
|
||||||
importTimestamp: Moment,
|
importTimestamp: Moment,
|
||||||
state: OnlineState,
|
state: OnlineState,
|
||||||
lastSeen: Moment,
|
lastSeen: Moment,
|
||||||
site: string,
|
site: Site,
|
||||||
domain: string,
|
domain: Domain,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodesParsingResult = {
|
export type NodesParsingResult = {
|
||||||
|
@ -220,12 +225,12 @@ export function parseNode(importTimestamp: Moment, nodeData: any): ParsedNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mac: mac,
|
mac: to(mac),
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: isOnline ? OnlineState.ONLINE : OnlineState.OFFLINE,
|
state: isOnline ? OnlineState.ONLINE : OnlineState.OFFLINE,
|
||||||
lastSeen: lastSeen,
|
lastSeen: lastSeen,
|
||||||
site: site || '<unknown-site>',
|
site: to(site || '<unknown-site>'), // FIXME: Handle this
|
||||||
domain: domain || '<unknown-domain>'
|
domain: to(domain || '<unknown-domain>') // FIXME: Handle this
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,7 +578,7 @@ export async function getAll(restParams: RestParams): Promise<{ total: number, m
|
||||||
return {monitoringStates, total};
|
return {monitoringStates, total};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getByMacs(macs: MAC[]): Promise<{ [key: string]: NodeStateData }> {
|
export async function getByMacs(macs: MAC[]): Promise<Record<string, NodeStateData>> {
|
||||||
if (_.isEmpty(macs)) {
|
if (_.isEmpty(macs)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -596,9 +601,9 @@ export async function getByMacs(macs: MAC[]): Promise<{ [key: string]: NodeState
|
||||||
return nodeStateByMac;
|
return nodeStateByMac;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirm(token: string): Promise<Node> {
|
export async function confirm(token: MonitoringToken): Promise<Node> {
|
||||||
const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
|
const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
|
||||||
if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) {
|
if (!node.monitoring || !nodeSecrets.monitoringToken || !equal(nodeSecrets.monitoringToken, token)) {
|
||||||
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,15 +617,15 @@ export async function confirm(token: string): Promise<Node> {
|
||||||
return newNode;
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disable(token: string): Promise<Node> {
|
export async function disable(token: MonitoringToken): Promise<Node> {
|
||||||
const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
|
const {node, nodeSecrets} = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
|
||||||
if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) {
|
if (!node.monitoring || !nodeSecrets.monitoringToken || !equal(nodeSecrets.monitoringToken, token)) {
|
||||||
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
throw {data: 'Invalid token.', type: ErrorTypes.badRequest};
|
||||||
}
|
}
|
||||||
|
|
||||||
node.monitoring = false;
|
node.monitoring = false;
|
||||||
node.monitoringConfirmed = false;
|
node.monitoringConfirmed = false;
|
||||||
nodeSecrets.monitoringToken = '';
|
nodeSecrets.monitoringToken = undefined;
|
||||||
|
|
||||||
const {node: newNode} = await NodeService.internalUpdateNode(node.token, node, nodeSecrets);
|
const {node: newNode} = await NodeService.internalUpdateNode(node.token, node, nodeSecrets);
|
||||||
return newNode;
|
return newNode;
|
||||||
|
|
|
@ -10,51 +10,69 @@ import Logger from "../logger";
|
||||||
import * as MailService from "../services/mailService";
|
import * as MailService from "../services/mailService";
|
||||||
import {normalizeString} from "../utils/strings";
|
import {normalizeString} from "../utils/strings";
|
||||||
import {monitoringConfirmUrl, monitoringDisableUrl} from "../utils/urlBuilder";
|
import {monitoringConfirmUrl, monitoringDisableUrl} from "../utils/urlBuilder";
|
||||||
import {FastdKey, MonitoringState, MonitoringToken, Node, NodeSecrets, NodeStatistics, UnixTimestampSeconds, Token} from "../types";
|
import {
|
||||||
|
FastdKey,
|
||||||
|
MAC,
|
||||||
|
MonitoringState,
|
||||||
|
MonitoringToken,
|
||||||
|
Node,
|
||||||
|
NodeSecrets,
|
||||||
|
NodeStatistics,
|
||||||
|
to,
|
||||||
|
Token,
|
||||||
|
unhandledEnumField,
|
||||||
|
UnixTimestampSeconds
|
||||||
|
} from "../types";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
|
||||||
const pglob = util.promisify(glob);
|
const pglob = util.promisify(glob);
|
||||||
|
|
||||||
type NodeFilter = {
|
type NodeFilter = {
|
||||||
|
// TODO: Newtype
|
||||||
hostname?: string,
|
hostname?: string,
|
||||||
mac?: string,
|
mac?: MAC,
|
||||||
key?: string,
|
key?: FastdKey,
|
||||||
token?: Token,
|
token?: Token,
|
||||||
monitoringToken?: string,
|
monitoringToken?: MonitoringToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Newtypes?
|
||||||
type NodeFilenameParsed = {
|
type NodeFilenameParsed = {
|
||||||
hostname?: string,
|
hostname?: string,
|
||||||
mac?: string,
|
mac?: string,
|
||||||
key?: string,
|
key?: string,
|
||||||
token?: Token,
|
token?: string,
|
||||||
monitoringToken?: string,
|
monitoringToken?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const linePrefixes = {
|
enum LINE_PREFIX {
|
||||||
hostname: '# Knotenname: ',
|
HOSTNAME = "# Knotenname: ",
|
||||||
nickname: '# Ansprechpartner: ',
|
NICKNAME = "# Ansprechpartner: ",
|
||||||
email: '# Kontakt: ',
|
EMAIL = "# Kontakt: ",
|
||||||
coords: '# Koordinaten: ',
|
COORDS = "# Koordinaten: ",
|
||||||
mac: '# MAC: ',
|
MAC = "# MAC: ",
|
||||||
token: '# Token: ',
|
TOKEN = "# Token: ",
|
||||||
monitoring: '# Monitoring: ',
|
MONITORING = "# Monitoring: ",
|
||||||
monitoringToken: '# Monitoring-Token: '
|
MONITORING_TOKEN = "# Monitoring-Token: ",
|
||||||
};
|
}
|
||||||
|
|
||||||
const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken'];
|
const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken'];
|
||||||
|
|
||||||
function generateToken(): Token {
|
function generateToken<Type extends { readonly __tag: symbol, value: any } =
|
||||||
return crypto.randomBytes(8).toString('hex');
|
{ readonly __tag: unique symbol, value: never }>(): Type {
|
||||||
|
return to<Type>(crypto.randomBytes(8).toString('hex'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNodeFilesPattern(filter: NodeFilter): string {
|
function toNodeFilesPattern(filter: NodeFilter): string {
|
||||||
const pattern = _.join(
|
const fields: (string | undefined)[] = [
|
||||||
_.map(
|
filter.hostname,
|
||||||
filenameParts,
|
filter.mac?.value,
|
||||||
field => field in filter ? (filter as {[key: string]: string | undefined})[field] : '*'),
|
filter.key?.value,
|
||||||
'@'
|
filter.token?.value,
|
||||||
);
|
filter.monitoringToken?.value,
|
||||||
|
];
|
||||||
|
|
||||||
|
const pattern = fields.map((value) => value || '*').join('@');
|
||||||
|
|
||||||
return config.server.peersPath + '/' + pattern.toLowerCase();
|
return config.server.peersPath + '/' + pattern.toLowerCase();
|
||||||
}
|
}
|
||||||
|
@ -104,7 +122,7 @@ function isDuplicate(filter: NodeFilter, token: Token | null): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseNodeFilename(files[0]).token !== token;
|
return parseNodeFilename(files[0]).token !== token.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNoDuplicates(token: Token | null, node: Node, nodeSecrets: NodeSecrets): void {
|
function checkNoDuplicates(token: Token | null, node: Node, nodeSecrets: NodeSecrets): void {
|
||||||
|
@ -138,6 +156,34 @@ function toNodeFilename(token: Token, node: Node, nodeSecrets: NodeSecrets): str
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeValue(prefix: LINE_PREFIX, node: Node, nodeSecrets: NodeSecrets): string {
|
||||||
|
switch (prefix) {
|
||||||
|
case LINE_PREFIX.HOSTNAME:
|
||||||
|
return node.hostname;
|
||||||
|
case LINE_PREFIX.NICKNAME:
|
||||||
|
return node.nickname;
|
||||||
|
case LINE_PREFIX.EMAIL:
|
||||||
|
return node.email;
|
||||||
|
case LINE_PREFIX.COORDS:
|
||||||
|
return node.coords || "";
|
||||||
|
case LINE_PREFIX.MAC:
|
||||||
|
return node.mac.value;
|
||||||
|
case LINE_PREFIX.TOKEN:
|
||||||
|
return node.token.value;
|
||||||
|
case LINE_PREFIX.MONITORING:
|
||||||
|
if (node.monitoring && node.monitoringConfirmed) {
|
||||||
|
return "aktiv";
|
||||||
|
} else if (node.monitoring && !node.monitoringConfirmed) {
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
case LINE_PREFIX.MONITORING_TOKEN:
|
||||||
|
return nodeSecrets.monitoringToken?.value || "";
|
||||||
|
default:
|
||||||
|
return unhandledEnumField(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function writeNodeFile(
|
async function writeNodeFile(
|
||||||
isUpdate: boolean,
|
isUpdate: boolean,
|
||||||
token: Token,
|
token: Token,
|
||||||
|
@ -146,35 +192,13 @@ async function writeNodeFile(
|
||||||
): Promise<{ token: Token, node: Node }> {
|
): Promise<{ token: Token, node: Node }> {
|
||||||
const filename = toNodeFilename(token, node, nodeSecrets);
|
const filename = toNodeFilename(token, node, nodeSecrets);
|
||||||
let data = '';
|
let data = '';
|
||||||
_.each(linePrefixes, function (prefix, key) {
|
|
||||||
let value;
|
|
||||||
switch (key) {
|
|
||||||
case 'monitoring':
|
|
||||||
if (node.monitoring && node.monitoringConfirmed) {
|
|
||||||
value = 'aktiv';
|
|
||||||
} else if (node.monitoring && !node.monitoringConfirmed) {
|
|
||||||
value = 'pending';
|
|
||||||
} else {
|
|
||||||
value = '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'monitoringToken':
|
for (const prefix of Object.values(LINE_PREFIX)) {
|
||||||
value = nodeSecrets.monitoringToken || '';
|
data += `${prefix}${getNodeValue(prefix, node, nodeSecrets)}\n`;
|
||||||
break;
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
value = key === 'token' ? token : (node as {[key: string]: any})[key];
|
|
||||||
if (_.isUndefined(value)) {
|
|
||||||
const nodeSecret = (nodeSecrets as {[key: string]: string})[key];
|
|
||||||
value = _.isUndefined(nodeSecret) ? '' : nodeSecret;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
data += prefix + value + '\n';
|
|
||||||
});
|
|
||||||
if (node.key) {
|
if (node.key) {
|
||||||
data += 'key "' + node.key + '";\n';
|
data += `key "${node.key}";\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// since node.js is single threaded we don't need a lock
|
// since node.js is single threaded we don't need a lock
|
||||||
|
@ -190,8 +214,7 @@ async function writeNodeFile(
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
try {
|
try {
|
||||||
oldFs.unlinkSync(file);
|
oldFs.unlinkSync(file);
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
Logger.tag('node', 'save').error('Could not delete old node file: ' + file, 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};
|
throw {data: 'Could not remove old node data.', type: ErrorTypes.internalError};
|
||||||
}
|
}
|
||||||
|
@ -202,8 +225,7 @@ async function writeNodeFile(
|
||||||
try {
|
try {
|
||||||
oldFs.writeFileSync(filename, data, 'utf8');
|
oldFs.writeFileSync(filename, data, 'utf8');
|
||||||
return {token, node};
|
return {token, node};
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
Logger.tag('node', 'save').error('Could not write node file: ' + filename, error);
|
Logger.tag('node', 'save').error('Could not write node file: ' + filename, error);
|
||||||
throw {data: 'Could not write node data.', type: ErrorTypes.internalError};
|
throw {data: 'Could not write node data.', type: ErrorTypes.internalError};
|
||||||
}
|
}
|
||||||
|
@ -213,8 +235,7 @@ async function deleteNodeFile(token: Token): Promise<void> {
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await findNodeFiles({token: token});
|
files = await findNodeFiles({token: token});
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
Logger.tag('node', 'delete').error('Could not find node file: ' + files, error);
|
Logger.tag('node', 'delete').error('Could not find node file: ' + files, error);
|
||||||
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
||||||
}
|
}
|
||||||
|
@ -225,82 +246,109 @@ async function deleteNodeFile(token: Token): Promise<void> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
oldFs.unlinkSync(files[0]);
|
oldFs.unlinkSync(files[0]);
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error);
|
Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error);
|
||||||
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
throw {data: 'Could not delete node.', type: ErrorTypes.internalError};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NodeBuilder {
|
||||||
|
public token: Token = to(""); // FIXME: Either make token optional in Node or handle this!
|
||||||
|
public nickname: string = "";
|
||||||
|
public email: string = "";
|
||||||
|
public hostname: string = ""; // FIXME: Either make hostname optional in Node or handle this!
|
||||||
|
public coords?: string;
|
||||||
|
public key?: FastdKey;
|
||||||
|
public mac: MAC = to(""); // FIXME: Either make mac optional in Node or handle this!
|
||||||
|
public monitoring: boolean = false;
|
||||||
|
public monitoringConfirmed: boolean = false;
|
||||||
|
public monitoringState: MonitoringState = MonitoringState.DISABLED;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly modifiedAt: UnixTimestampSeconds,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Node {
|
||||||
|
return {
|
||||||
|
token: this.token,
|
||||||
|
nickname: this.nickname,
|
||||||
|
email: this.email,
|
||||||
|
hostname: this.hostname,
|
||||||
|
coords: this.coords,
|
||||||
|
key: this.key,
|
||||||
|
mac: this.mac,
|
||||||
|
monitoring: this.monitoring,
|
||||||
|
monitoringConfirmed: this.monitoringConfirmed,
|
||||||
|
monitoringState: this.monitoringState,
|
||||||
|
modifiedAt: this.modifiedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodeValue(prefix: LINE_PREFIX, node: NodeBuilder, nodeSecrets: NodeSecrets, value: string) {
|
||||||
|
switch (prefix) {
|
||||||
|
case LINE_PREFIX.HOSTNAME:
|
||||||
|
node.hostname = value;
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.NICKNAME:
|
||||||
|
node.nickname = value;
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.EMAIL:
|
||||||
|
node.email = value;
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.COORDS:
|
||||||
|
node.coords = value;
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.MAC:
|
||||||
|
node.mac = to(value);
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.TOKEN:
|
||||||
|
node.token = to(value);
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.MONITORING:
|
||||||
|
const active = value === 'aktiv';
|
||||||
|
const pending = value === 'pending';
|
||||||
|
node.monitoring = active || pending;
|
||||||
|
node.monitoringConfirmed = active;
|
||||||
|
node.monitoringState =
|
||||||
|
active ? MonitoringState.ACTIVE : (pending ? MonitoringState.PENDING : MonitoringState.DISABLED);
|
||||||
|
break;
|
||||||
|
case LINE_PREFIX.MONITORING_TOKEN:
|
||||||
|
nodeSecrets.monitoringToken = to<MonitoringToken>(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return unhandledEnumField(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function parseNodeFile(file: string): Promise<{ node: Node, nodeSecrets: NodeSecrets }> {
|
async function parseNodeFile(file: string): Promise<{ node: Node, nodeSecrets: NodeSecrets }> {
|
||||||
const contents = await fs.readFile(file);
|
const contents = await fs.readFile(file);
|
||||||
const stats = await fs.lstat(file);
|
const stats = await fs.lstat(file);
|
||||||
const modifiedAt = Math.floor(stats.mtimeMs / 1000);
|
const modifiedAt = Math.floor(stats.mtimeMs / 1000);
|
||||||
|
|
||||||
const lines = contents.toString();
|
const lines = contents.toString().split("\n");
|
||||||
|
|
||||||
const node: {[key: string]: any} = {};
|
const node = new NodeBuilder(modifiedAt);
|
||||||
const nodeSecrets: {[key: string]: any} = {};
|
const nodeSecrets: NodeSecrets = {};
|
||||||
|
|
||||||
_.each(lines.split('\n'), function (line) {
|
for (const line of lines) {
|
||||||
const entries: {[key: string]: string} = {};
|
if (line.substring(0, 5) === 'key "') {
|
||||||
|
node.key = to<FastdKey>(normalizeString(line.split('"')[1]));
|
||||||
for (const key of Object.keys(linePrefixes)) {
|
} else {
|
||||||
const prefix = (linePrefixes as {[key: string]: string})[key];
|
for (const prefix of Object.values(LINE_PREFIX)) {
|
||||||
if (line.substring(0, prefix.length) === prefix) {
|
if (line.substring(0, prefix.length) === prefix) {
|
||||||
entries[key] = normalizeString(line.substr(prefix.length));
|
const value = normalizeString(line.substring(prefix.length));
|
||||||
|
setNodeValue(prefix, node, nodeSecrets, value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.isEmpty(entries) && line.substring(0, 5) === 'key "') {
|
|
||||||
entries.key = normalizeString(line.split('"')[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(entries, function (value, key) {
|
|
||||||
switch (key) {
|
|
||||||
case 'mac':
|
|
||||||
node.mac = value;
|
|
||||||
node.mapId = _.toLower(value).replace(/:/g, '');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'monitoring':
|
|
||||||
const active = value === 'aktiv';
|
|
||||||
const pending = value === 'pending';
|
|
||||||
node.monitoring = active || pending;
|
|
||||||
node.monitoringConfirmed = active;
|
|
||||||
node.monitoringState =
|
|
||||||
active ? MonitoringState.ACTIVE : (pending ? MonitoringState.PENDING : MonitoringState.DISABLED);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'monitoringToken':
|
|
||||||
nodeSecrets.monitoringToken = value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
node[key] = value;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: {
|
node: node.build(),
|
||||||
token: node.token as Token || '',
|
nodeSecrets,
|
||||||
nickname: node.nickname as string || '',
|
|
||||||
email: node.email as string || '',
|
|
||||||
hostname: node.hostname as string || '',
|
|
||||||
coords: node.coords as string || undefined,
|
|
||||||
key: node.key as FastdKey || undefined,
|
|
||||||
mac: node.mac as string || '',
|
|
||||||
monitoring: !!node.monitoring,
|
|
||||||
monitoringConfirmed: !!node.monitoringConfirmed,
|
|
||||||
monitoringState: node.monitoringState as MonitoringState || MonitoringState.DISABLED,
|
|
||||||
modifiedAt,
|
|
||||||
},
|
|
||||||
nodeSecrets: {
|
|
||||||
monitoringToken: nodeSecrets.monitoringToken as MonitoringToken || undefined,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,13 +397,13 @@ async function sendMonitoringConfirmationMail(node: Node, nodeSecrets: NodeSecre
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createNode(node: Node): Promise<{ token: Token, node: Node }> {
|
export async function createNode(node: Node): Promise<{ token: Token, node: Node }> {
|
||||||
const token = generateToken();
|
const token: Token = generateToken();
|
||||||
const nodeSecrets: NodeSecrets = {};
|
const nodeSecrets: NodeSecrets = {};
|
||||||
|
|
||||||
node.monitoringConfirmed = false;
|
node.monitoringConfirmed = false;
|
||||||
|
|
||||||
if (node.monitoring) {
|
if (node.monitoring) {
|
||||||
nodeSecrets.monitoringToken = generateToken();
|
nodeSecrets.monitoringToken = generateToken<MonitoringToken>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const written = await writeNodeFile(false, token, node, nodeSecrets);
|
const written = await writeNodeFile(false, token, node, nodeSecrets);
|
||||||
|
@ -371,13 +419,13 @@ export async function updateNode (token: Token, node: Node): Promise<{token: Tok
|
||||||
const {node: currentNode, nodeSecrets} = await getNodeDataWithSecretsByToken(token);
|
const {node: currentNode, nodeSecrets} = await getNodeDataWithSecretsByToken(token);
|
||||||
|
|
||||||
let monitoringConfirmed = false;
|
let monitoringConfirmed = false;
|
||||||
let monitoringToken = '';
|
let monitoringToken: MonitoringToken | undefined;
|
||||||
|
|
||||||
if (node.monitoring) {
|
if (node.monitoring) {
|
||||||
if (!currentNode.monitoring) {
|
if (!currentNode.monitoring) {
|
||||||
// monitoring just has been enabled
|
// monitoring just has been enabled
|
||||||
monitoringConfirmed = false;
|
monitoringConfirmed = false;
|
||||||
monitoringToken = generateToken();
|
monitoringToken = generateToken<MonitoringToken>();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// monitoring is still enabled
|
// monitoring is still enabled
|
||||||
|
@ -385,12 +433,12 @@ export async function updateNode (token: Token, node: Node): Promise<{token: Tok
|
||||||
if (currentNode.email !== node.email) {
|
if (currentNode.email !== node.email) {
|
||||||
// new email so we need a new token and a reconfirmation
|
// new email so we need a new token and a reconfirmation
|
||||||
monitoringConfirmed = false;
|
monitoringConfirmed = false;
|
||||||
monitoringToken = generateToken();
|
monitoringToken = generateToken<MonitoringToken>();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// email unchanged, keep token (fix if not set) and confirmation state
|
// email unchanged, keep token (fix if not set) and confirmation state
|
||||||
monitoringConfirmed = currentNode.monitoringConfirmed;
|
monitoringConfirmed = currentNode.monitoringConfirmed;
|
||||||
monitoringToken = nodeSecrets.monitoringToken || generateToken();
|
monitoringToken = nodeSecrets.monitoringToken || generateToken<MonitoringToken>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,12 +488,12 @@ export async function getAllNodes(): Promise<Node[]> {
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeDataWithSecretsByMac (mac: string): Promise<{node: Node, nodeSecrets: NodeSecrets} | null> {
|
export async function getNodeDataWithSecretsByMac(mac: MAC): Promise<{ node: Node, nodeSecrets: NodeSecrets } | null> {
|
||||||
return await findNodeDataByFilePattern({ mac: mac });
|
return await findNodeDataByFilePattern({mac});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeDataByMac (mac: string): Promise<Node | null> {
|
export async function getNodeDataByMac(mac: MAC): Promise<Node | null> {
|
||||||
const result = await findNodeDataByFilePattern({ mac: mac });
|
const result = await findNodeDataByFilePattern({mac});
|
||||||
return result ? result.node : null;
|
return result ? result.node : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,8 +529,7 @@ export async function fixNodeFilenames(): Promise<void> {
|
||||||
if (file !== expectedFilename) {
|
if (file !== expectedFilename) {
|
||||||
try {
|
try {
|
||||||
await fs.rename(file, expectedFilename);
|
await fs.rename(file, expectedFilename);
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + error
|
'Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + error
|
||||||
);
|
);
|
||||||
|
@ -518,10 +565,6 @@ export async function getNodeStatistics(): Promise<NodeStatistics> {
|
||||||
nodeStatistics.withCoords += 1;
|
nodeStatistics.withCoords += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureExhaustive(monitoringState: never): void {
|
|
||||||
throw new Error('Add missing case for monitoring stat below: ' + monitoringState);
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitoringState = node.monitoringState;
|
const monitoringState = node.monitoringState;
|
||||||
switch (monitoringState) {
|
switch (monitoringState) {
|
||||||
case MonitoringState.ACTIVE:
|
case MonitoringState.ACTIVE:
|
||||||
|
@ -535,7 +578,7 @@ export async function getNodeStatistics(): Promise<NodeStatistics> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ensureExhaustive(monitoringState);
|
unhandledEnumField(monitoringState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,32 @@
|
||||||
import {ArrayField, Field, RawJsonField} from "sparkson"
|
import {ArrayField, Field, RawJsonField} from "sparkson"
|
||||||
import {ClientConfig} from "./shared";
|
import {ClientConfig, to} from "./shared";
|
||||||
|
|
||||||
// TODO: Replace string types by more specific types like URL, Password, etc.
|
// TODO: Replace string types by more specific types like URL, Password, etc.
|
||||||
|
|
||||||
export type Username = string;
|
export type Username = {
|
||||||
export type CleartextPassword = string;
|
value: string;
|
||||||
export type PasswordHash = string;
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export type CleartextPassword = {
|
||||||
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export type PasswordHash = {
|
||||||
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
|
||||||
export class UsersConfig {
|
export class UsersConfig {
|
||||||
|
public username: Username;
|
||||||
|
public passwordHash: PasswordHash;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Field("user") public username: Username,
|
@Field("user") username: string,
|
||||||
@Field("passwordHash") public passwordHash: PasswordHash,
|
@Field("passwordHash") passwordHash: string,
|
||||||
) {}
|
) {
|
||||||
|
this.username = to(username);
|
||||||
|
this.passwordHash = to(passwordHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoggingConfig {
|
export class LoggingConfig {
|
||||||
|
|
|
@ -6,10 +6,37 @@ export type TypeGuard<T> = (arg: unknown) => arg is T;
|
||||||
export type EnumValue<E> = E[keyof E];
|
export type EnumValue<E> = E[keyof E];
|
||||||
export type EnumTypeGuard<E> = TypeGuard<EnumValue<E>>;
|
export type EnumTypeGuard<E> = TypeGuard<EnumValue<E>>;
|
||||||
|
|
||||||
|
export function unhandledEnumField(field: never): never {
|
||||||
|
throw new Error(`Unhandled enum field: ${field}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function to<Type extends { readonly __tag: symbol, value: any } = { readonly __tag: unique symbol, value: never }>(value: Type['value']): Type {
|
||||||
|
return value as any as Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lift2<Result, Type extends { readonly __tag: symbol, value: any }>(callback: (a: Type["value"], b: Type["value"]) => Result): (newtype1: Type, newtype2: Type) => Result {
|
||||||
|
return (a, b) => callback(a.value, b.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equal<Result, Type extends { readonly __tag: symbol, value: any }>(a: Type, b: Type): boolean {
|
||||||
|
return lift2((a, b) => a === b)(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
export function isObject(arg: unknown): arg is object {
|
export function isObject(arg: unknown): arg is object {
|
||||||
return arg !== null && typeof arg === "object";
|
return arg !== null && typeof arg === "object";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toIsNewtype<Type extends { readonly __tag: symbol, value: Value } = { readonly __tag: unique symbol, value: never }, Value = any>(isValue: TypeGuard<Value>): TypeGuard<Type> {
|
||||||
|
// TODO: Add validation pattern.
|
||||||
|
return (arg: unknown): arg is Type => {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const newtype = arg as Type;
|
||||||
|
return isValue(newtype.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
||||||
if (!Array.isArray(arg)) {
|
if (!Array.isArray(arg)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -248,19 +275,32 @@ export function isClientConfig(arg: unknown): arg is ClientConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Token type.
|
// TODO: Token type.
|
||||||
export type Token = string;
|
export type Token = {
|
||||||
export const isToken = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export const isToken = toIsNewtype<Token>(isString);
|
||||||
|
|
||||||
export type FastdKey = string;
|
export type FastdKey = {
|
||||||
export const isFastdKey = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export const isFastdKey = toIsNewtype<FastdKey>(isString);
|
||||||
|
|
||||||
export type MAC = string;
|
export type MAC = {
|
||||||
export const isMAC = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export const isMAC = toIsNewtype<MAC>(isString);
|
||||||
|
|
||||||
export type UnixTimestampSeconds = number;
|
export type UnixTimestampSeconds = number;
|
||||||
export type UnixTimestampMilliseconds = number;
|
export type UnixTimestampMilliseconds = number;
|
||||||
|
|
||||||
export type MonitoringToken = string;
|
export type MonitoringToken = {
|
||||||
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
|
||||||
export enum MonitoringState {
|
export enum MonitoringState {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
PENDING = "pending",
|
PENDING = "pending",
|
||||||
|
@ -269,9 +309,12 @@ export enum MonitoringState {
|
||||||
|
|
||||||
export const isMonitoringState = toIsEnum(MonitoringState);
|
export const isMonitoringState = toIsEnum(MonitoringState);
|
||||||
|
|
||||||
export type NodeId = string;
|
export type NodeId = {
|
||||||
export const isNodeId = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: More Newtypes
|
||||||
export interface Node {
|
export interface Node {
|
||||||
token: Token;
|
token: Token;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
|
@ -310,13 +353,20 @@ export enum OnlineState {
|
||||||
ONLINE = "ONLINE",
|
ONLINE = "ONLINE",
|
||||||
OFFLINE = "OFFLINE",
|
OFFLINE = "OFFLINE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isOnlineState = toIsEnum(OnlineState);
|
export const isOnlineState = toIsEnum(OnlineState);
|
||||||
|
|
||||||
export type Site = string;
|
export type Site = {
|
||||||
export const isSite = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export const isSite = toIsNewtype<Site>(isString);
|
||||||
|
|
||||||
export type Domain = string;
|
export type Domain = {
|
||||||
export const isDomain = isString;
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
export const isDomain = toIsNewtype<Domain>(isString);
|
||||||
|
|
||||||
export interface EnhancedNode extends Node {
|
export interface EnhancedNode extends Node {
|
||||||
site?: Site,
|
site?: Site,
|
||||||
|
@ -426,7 +476,10 @@ export enum MailSortField {
|
||||||
|
|
||||||
export const isMailSortField = toIsEnum(MailSortField);
|
export const isMailSortField = toIsEnum(MailSortField);
|
||||||
|
|
||||||
export type GenericSortField = string;
|
export type GenericSortField = {
|
||||||
|
value: string;
|
||||||
|
readonly __tag: unique symbol
|
||||||
|
};
|
||||||
|
|
||||||
export enum SortDirection {
|
export enum SortDirection {
|
||||||
ASCENDING = "ASC",
|
ASCENDING = "ASC",
|
||||||
|
|
|
@ -32,9 +32,9 @@ export function editNodeUrl (): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function monitoringConfirmUrl(monitoringToken: MonitoringToken): string {
|
export function monitoringConfirmUrl(monitoringToken: MonitoringToken): string {
|
||||||
return formUrl('monitoring/confirm', { token: monitoringToken });
|
return formUrl('monitoring/confirm', {token: monitoringToken.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function monitoringDisableUrl(monitoringToken: MonitoringToken): string {
|
export function monitoringDisableUrl(monitoringToken: MonitoringToken): string {
|
||||||
return formUrl('monitoring/disable', { token: monitoringToken });
|
return formUrl('monitoring/disable', {token: monitoringToken.value});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue