2022-05-19 13:33:48 +02:00
|
|
|
import {ArrayField, Field, RawJsonField} from "sparkson";
|
|
|
|
|
2022-02-22 15:39:39 +01:00
|
|
|
// Types shared with the client.
|
2022-05-26 13:58:01 +02:00
|
|
|
export type TypeGuard<T> = (arg: unknown) => arg is T;
|
2022-02-22 15:39:39 +01:00
|
|
|
|
|
|
|
export function isObject(arg: unknown): arg is object {
|
|
|
|
return arg !== null && typeof arg === "object";
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:58:01 +02:00
|
|
|
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
2022-05-19 13:33:48 +02:00
|
|
|
if (!Array.isArray(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
for (const element of arg) {
|
|
|
|
if (!isT(element)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isString(arg: unknown): arg is string {
|
|
|
|
return typeof arg === "string"
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:58:01 +02:00
|
|
|
export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> {
|
|
|
|
return (arg): arg is T[] => isArray(arg, isT);
|
|
|
|
}
|
2022-02-22 15:39:39 +01:00
|
|
|
|
2022-05-26 13:58:01 +02:00
|
|
|
export function toIsEnum<E>(enumDef: E): TypeGuard<E> {
|
|
|
|
return (arg): arg is E => Object.values(enumDef).includes(arg as [keyof E]);
|
2022-02-22 15:39:39 +01:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:58:01 +02:00
|
|
|
export type Version = string;
|
|
|
|
|
|
|
|
// Should be good enough for now.
|
|
|
|
export const isVersion = isString;
|
|
|
|
|
2022-02-22 15:39:39 +01:00
|
|
|
export type NodeStatistics = {
|
|
|
|
registered: number;
|
|
|
|
withVPN: number;
|
|
|
|
withCoords: number;
|
|
|
|
monitoring: {
|
|
|
|
active: number;
|
|
|
|
pending: number;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const stats = arg as NodeStatistics;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-02-22 15:39:39 +01:00
|
|
|
return (
|
|
|
|
typeof stats.registered === "number" &&
|
|
|
|
typeof stats.withVPN === "number" &&
|
|
|
|
typeof stats.withCoords === "number" &&
|
|
|
|
typeof stats.monitoring === "object" &&
|
|
|
|
typeof stats.monitoring.active === "number" &&
|
|
|
|
typeof stats.monitoring.pending === "number"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Statistics {
|
|
|
|
nodes: NodeStatistics;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isStatistics(arg: unknown): arg is Statistics {
|
|
|
|
return isObject(arg) && isNodeStatistics((arg as Statistics).nodes);
|
|
|
|
}
|
2022-05-19 13:33:48 +02:00
|
|
|
|
|
|
|
export class CommunityConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("name") public name: string,
|
|
|
|
@Field("domain") public domain: string,
|
|
|
|
@Field("contactEmail") public contactEmail: string,
|
|
|
|
@ArrayField("sites", String) public sites: string[],
|
|
|
|
@ArrayField("domains", String) public domains: string[],
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as CommunityConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
typeof cfg.name === "string" &&
|
|
|
|
typeof cfg.domain === "string" &&
|
|
|
|
typeof cfg.contactEmail === "string" &&
|
|
|
|
isArray(cfg.sites, isString) &&
|
|
|
|
isArray(cfg.domains, isString)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LegalConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("privacyUrl", true) public privacyUrl?: string,
|
|
|
|
@Field("imprintUrl", true) public imprintUrl?: string,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isLegalConfig(arg: unknown): arg is LegalConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as LegalConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
(cfg.privacyUrl === undefined || typeof cfg.privacyUrl === "string") &&
|
|
|
|
(cfg.imprintUrl === undefined || typeof cfg.imprintUrl === "string")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ClientMapConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("mapUrl") public mapUrl: string,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as ClientMapConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return typeof cfg.mapUrl === "string";
|
|
|
|
}
|
|
|
|
|
|
|
|
export class MonitoringConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("enabled") public enabled: boolean,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as MonitoringConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return typeof cfg.enabled === "boolean";
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Coords {
|
|
|
|
constructor(
|
|
|
|
@Field("lat") public lat: number,
|
|
|
|
@Field("lng") public lng: number,
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isCoords(arg: unknown): arg is Coords {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const coords = arg as Coords;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
typeof coords.lat === "number" &&
|
|
|
|
typeof coords.lng === "number"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class CoordsSelectorConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("lat") public lat: number,
|
|
|
|
@Field("lng") public lng: number,
|
|
|
|
@Field("defaultZoom") public defaultZoom: number,
|
|
|
|
@RawJsonField("layers") public layers: any, // TODO: Better types!
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isCoordsSelectorConfig(arg: unknown): arg is CoordsSelectorConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as CoordsSelectorConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
typeof cfg.lat === "number" &&
|
|
|
|
typeof cfg.lng === "number" &&
|
|
|
|
typeof cfg.defaultZoom === "number" &&
|
|
|
|
isObject(cfg.layers) // TODO: Better types!
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class OtherCommunityInfoConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("showInfo") public showInfo: boolean,
|
|
|
|
@Field("showBorderForDebugging") public showBorderForDebugging: boolean,
|
|
|
|
@ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[],
|
|
|
|
) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityInfoConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as OtherCommunityInfoConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
typeof cfg.showInfo === "boolean" &&
|
|
|
|
typeof cfg.showBorderForDebugging === "boolean" &&
|
|
|
|
isArray(cfg.localCommunityPolygon, isCoords)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ClientConfig {
|
|
|
|
constructor(
|
|
|
|
@Field("community") public community: CommunityConfig,
|
|
|
|
@Field("legal") public legal: LegalConfig,
|
|
|
|
@Field("map") public map: ClientMapConfig,
|
|
|
|
@Field("monitoring") public monitoring: MonitoringConfig,
|
|
|
|
@Field("coordsSelector") public coordsSelector: CoordsSelectorConfig,
|
|
|
|
@Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig,
|
|
|
|
@Field("rootPath", true, undefined, "/") public rootPath: string,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isClientConfig(arg: unknown): arg is ClientConfig {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const cfg = arg as ClientConfig;
|
2022-05-26 13:03:46 +02:00
|
|
|
// noinspection SuspiciousTypeOfGuard
|
2022-05-19 13:33:48 +02:00
|
|
|
return (
|
|
|
|
isCommunityConfig(cfg.community) &&
|
|
|
|
isLegalConfig(cfg.legal) &&
|
|
|
|
isClientMapConfig(cfg.map) &&
|
|
|
|
isMonitoringConfig(cfg.monitoring) &&
|
|
|
|
isCoordsSelectorConfig(cfg.coordsSelector) &&
|
|
|
|
isOtherCommunityInfoConfig(cfg.otherCommunityInfo) &&
|
|
|
|
typeof cfg.rootPath === "string"
|
|
|
|
);
|
|
|
|
}
|
2022-05-26 13:58:01 +02:00
|
|
|
|
|
|
|
// TODO: Token type.
|
|
|
|
export type Token = string;
|
|
|
|
export const isToken = isString;
|
|
|
|
|
|
|
|
export type FastdKey = string;
|
|
|
|
export const isFastdKey = isString;
|
|
|
|
|
|
|
|
export type MAC = string;
|
|
|
|
export const isMAC = isString;
|
|
|
|
|
|
|
|
export type UnixTimestampSeconds = number;
|
|
|
|
export type UnixTimestampMilliseconds = number;
|
|
|
|
|
|
|
|
export type MonitoringToken = string;
|
|
|
|
export enum MonitoringState {
|
|
|
|
ACTIVE = "active",
|
|
|
|
PENDING = "pending",
|
|
|
|
DISABLED = "disabled",
|
|
|
|
}
|
|
|
|
|
|
|
|
export const isMonitoringState = toIsEnum(MonitoringState);
|
|
|
|
|
|
|
|
export type NodeId = string;
|
|
|
|
export const isNodeId = isString;
|
|
|
|
|
|
|
|
export interface Node {
|
|
|
|
token: Token;
|
|
|
|
nickname: string;
|
|
|
|
email: string;
|
|
|
|
hostname: string;
|
|
|
|
coords?: string; // TODO: Use object with longitude and latitude.
|
|
|
|
key?: FastdKey;
|
|
|
|
mac: MAC;
|
|
|
|
monitoring: boolean;
|
|
|
|
monitoringConfirmed: boolean;
|
|
|
|
monitoringState: MonitoringState;
|
|
|
|
modifiedAt: UnixTimestampSeconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isNode(arg: unknown): arg is Node {
|
|
|
|
if (!isObject(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const node = arg as Node;
|
|
|
|
// noinspection SuspiciousTypeOfGuard
|
|
|
|
return (
|
|
|
|
isToken(node.token) &&
|
|
|
|
typeof node.nickname === "string" &&
|
|
|
|
typeof node.email === "string" &&
|
|
|
|
typeof node.hostname === "string" &&
|
|
|
|
(node.coords === undefined || typeof node.coords === "string") &&
|
|
|
|
(node.key === undefined || isFastdKey(node.key)) &&
|
|
|
|
isMAC(node.mac) &&
|
|
|
|
typeof node.monitoring === "boolean" &&
|
|
|
|
typeof node.monitoringConfirmed === "boolean" &&
|
|
|
|
isMonitoringState(node.monitoringState) &&
|
|
|
|
typeof node.modifiedAt === "number"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum OnlineState {
|
|
|
|
ONLINE = "ONLINE",
|
|
|
|
OFFLINE = "OFFLINE",
|
|
|
|
}
|
|
|
|
export const isOnlineState = toIsEnum(OnlineState);
|
|
|
|
|
|
|
|
export type Site = string;
|
|
|
|
export const isSite = isString;
|
|
|
|
|
|
|
|
export type Domain = string;
|
|
|
|
export const isDomain = isString;
|
|
|
|
|
|
|
|
export interface EnhancedNode extends Node {
|
|
|
|
site?: Site,
|
|
|
|
domain?: Domain,
|
|
|
|
onlineState?: OnlineState,
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
|
|
|
|
if (!isNode(arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const node = arg as EnhancedNode;
|
|
|
|
return (
|
|
|
|
(node.site === undefined || isSite(node.site)) &&
|
|
|
|
(node.domain === undefined || isDomain(node.domain)) &&
|
|
|
|
(node.onlineState === undefined || isOnlineState(node.onlineState))
|
|
|
|
);
|
|
|
|
}
|