User Server Udpate
This commit is contained in:
parent
185e004839
commit
b3fc54ded2
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { TextTool } from "../text/TextTool";
|
||||||
|
|
||||||
|
export class DateFormatter
|
||||||
|
{
|
||||||
|
static YMD_HMS( date:Date ):string
|
||||||
|
{
|
||||||
|
let ye = ( date.getFullYear() + "" ).substring( 2 );
|
||||||
|
let mo = TextTool.prependZeros( ( date.getMonth() + 1 ) );
|
||||||
|
let da = TextTool.prependZeros( date.getDate() );
|
||||||
|
|
||||||
|
let h = TextTool.prependZeros( date.getHours() );
|
||||||
|
let m = TextTool.prependZeros( date.getMinutes() );
|
||||||
|
let s = TextTool.prependZeros( date.getSeconds() );
|
||||||
|
|
||||||
|
return `${ye}-${mo}-${da} ${h}-${m}-${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static forUsers( date:Date ):string
|
||||||
|
{
|
||||||
|
let ye = ( date.getFullYear() + "" );
|
||||||
|
let mo = TextTool.prependZeros( ( date.getMonth() + 1 ) );
|
||||||
|
let da = TextTool.prependZeros( date.getDate() );
|
||||||
|
|
||||||
|
let h = TextTool.prependZeros( date.getHours() );
|
||||||
|
let m = TextTool.prependZeros( date.getMinutes() );
|
||||||
|
let s = TextTool.prependZeros( date.getSeconds() );
|
||||||
|
|
||||||
|
return `${ye}-${mo}-${da} ${h}:${m}:${s}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,11 @@ export class DateHelper
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static nowMS()
|
||||||
|
{
|
||||||
|
return this.now().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
static today()
|
static today()
|
||||||
{
|
{
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,11 @@ export class DateMath
|
||||||
return this.addMilliseconds( a, duration * 1000 );
|
return this.addMilliseconds( a, duration * 1000 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static durationToHours( duration:number )
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
static addMinutes( a:Date, durationMinutes:number )
|
static addMinutes( a:Date, durationMinutes:number )
|
||||||
{
|
{
|
||||||
return this.addSeconds( a, durationMinutes * 60 );
|
return this.addSeconds( a, durationMinutes * 60 );
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
export class Duration
|
||||||
|
{
|
||||||
|
static toMinutes( duration:number )
|
||||||
|
{
|
||||||
|
return duration / 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMinutes( minutes:number )
|
||||||
|
{
|
||||||
|
return minutes * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
static toHours( duration:number )
|
||||||
|
{
|
||||||
|
return this.toMinutes( duration ) / 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHours( hours:number )
|
||||||
|
{
|
||||||
|
return this.fromMinutes( hours * 60 );
|
||||||
|
}
|
||||||
|
|
||||||
|
static toDays( duration:number )
|
||||||
|
{
|
||||||
|
return this.toHours( duration ) / 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromDays( days:number )
|
||||||
|
{
|
||||||
|
return this.fromHours( days * 24 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { DateHelper } from "./DateHelper";
|
||||||
|
|
||||||
|
export class ISOTimeStamp
|
||||||
|
{
|
||||||
|
value:string;
|
||||||
|
|
||||||
|
static toDate( stamp:ISOTimeStamp ):Date
|
||||||
|
{
|
||||||
|
return new Date( stamp.value );
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromDate( date:Date ):ISOTimeStamp
|
||||||
|
{
|
||||||
|
let stamp = new ISOTimeStamp();
|
||||||
|
stamp.value = date.toISOString();
|
||||||
|
return stamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static now():ISOTimeStamp
|
||||||
|
{
|
||||||
|
return ISOTimeStamp.fromDate( DateHelper.now() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -202,4 +202,9 @@ export class ClassFlag
|
||||||
|
|
||||||
ClassFlag.setClass( element, classString, ! hasClassAssigned );
|
ClassFlag.setClass( element, classString, ! hasClassAssigned );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static queryElement( element:Element, classNAme:string )
|
||||||
|
{
|
||||||
|
return new ClassFlag( classNAme ).query( element );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,13 @@ import { TreeWalker } from "../graphs/TreeWalker";
|
||||||
|
|
||||||
export class DOMEditor
|
export class DOMEditor
|
||||||
{
|
{
|
||||||
|
static setText( element:Element, text:string, doc:Document = null )
|
||||||
|
{
|
||||||
|
doc = doc || document;
|
||||||
|
element.innerHTML = "";
|
||||||
|
element.appendChild( doc.createTextNode( text ) );
|
||||||
|
}
|
||||||
|
|
||||||
static nodeListToArray( list:NodeList )
|
static nodeListToArray( list:NodeList )
|
||||||
{
|
{
|
||||||
return Array.prototype.slice.call( list );
|
return Array.prototype.slice.call( list );
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Message } from "./Message";
|
||||||
|
|
||||||
|
export class DataMessage<T> extends Message
|
||||||
|
{
|
||||||
|
data:T;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class TextTool
|
||||||
|
{
|
||||||
|
static prependZeros( anySource:string|number, minimumLength:number = 2 )
|
||||||
|
{
|
||||||
|
let source = anySource + "";
|
||||||
|
|
||||||
|
while ( source.length < minimumLength )
|
||||||
|
{
|
||||||
|
source = "0" + source;
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@ import { RegExpUtility } from "../RegExpUtitlity";
|
||||||
export type Variables = {[index:string]:string }
|
export type Variables = {[index:string]:string }
|
||||||
export class VariableReplacer
|
export class VariableReplacer
|
||||||
{
|
{
|
||||||
static replace( source:string, variables:Variables )
|
static replace( source:string, variables:Variables, prefix = "${", postfix = "}" )
|
||||||
{
|
{
|
||||||
for ( let it in variables )
|
for ( let it in variables )
|
||||||
{
|
{
|
||||||
let regexSource = RegExpUtility.toRegexSource( "${" + it + "}" );
|
let regexSource = RegExpUtility.toRegexSource( prefix + it + postfix );
|
||||||
source = source.replace( new RegExp( regexSource, "g" ), variables[ it ] );
|
source = source.replace( new RegExp( regexSource, "g" ), variables[ it ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,14 @@ export class Arrays
|
||||||
Arrays.insert( array, element, 0 );
|
Arrays.insert( array, element, 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static shiftToSize<T>( array:T[], maxSize:number )
|
||||||
|
{
|
||||||
|
while ( array.length > maxSize )
|
||||||
|
{
|
||||||
|
array.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static remove<T>( array:T[], element:T )
|
static remove<T>( array:T[], element:T )
|
||||||
{
|
{
|
||||||
if ( ! array || ! element )
|
if ( ! array || ! element )
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Arrays } from "./Arrays";
|
||||||
|
|
||||||
export class MapList
|
export class MapList
|
||||||
{
|
{
|
||||||
static add<K,V>( map:Map<K,V[]>, k:K, v:V )
|
static add<K,V>( map:Map<K,V[]>, k:K, v:V )
|
||||||
|
|
@ -10,5 +12,16 @@ export class MapList
|
||||||
map.get( k ).push( v );
|
map.get( k ).push( v );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static shiftToSize<K,V>( map:Map<K,V[]>, k:K, num:number )
|
||||||
|
{
|
||||||
|
if ( ! map.has( k ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let array = map.get( k );
|
||||||
|
|
||||||
|
Arrays.shiftToSize( array, num );
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
export class Request
|
||||||
|
{
|
||||||
|
static post<I,O>( url:string, input:I ):Promise<O>
|
||||||
|
{
|
||||||
|
let promise = new Promise<O>
|
||||||
|
(
|
||||||
|
( resolve, reject ) =>
|
||||||
|
{
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
console.log( "post", url, ">>", input);
|
||||||
|
xhr.open( "POST", url, true );
|
||||||
|
|
||||||
|
xhr.responseType = "text";
|
||||||
|
|
||||||
|
xhr.onload=
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
console.log( xhr.responseURL, xhr.responseText );
|
||||||
|
|
||||||
|
if ( xhr.status !== 200 || xhr.responseText.startsWith( "ERROR:" ) )
|
||||||
|
{
|
||||||
|
reject( xhr.responseText )
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resolve( JSON.parse( xhr.responseText ) as O );
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror=(e)=>
|
||||||
|
{
|
||||||
|
reject( e );
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send( JSON.stringify( input ) );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class CryptContainer
|
||||||
|
{
|
||||||
|
key:string;
|
||||||
|
data:string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
import { CryptContainer } from "./CryptContainer";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
import { CryptSettings } from "./CryptSettings";
|
||||||
|
import { Files } from "../files/Files";
|
||||||
|
import { RJLog } from "../log/RJLog";
|
||||||
|
|
||||||
|
|
||||||
|
export class CryptIO
|
||||||
|
{
|
||||||
|
static readonly encryptionSuffix = ".crypt";
|
||||||
|
static readonly debugOutput = true;
|
||||||
|
static readonly debugOutputSuffix = ".crypt.json";
|
||||||
|
|
||||||
|
private static _cryptSettings:CryptSettings;
|
||||||
|
|
||||||
|
static set cryptSettings( cryptSettings:CryptSettings )
|
||||||
|
{
|
||||||
|
this._cryptSettings = cryptSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createUUID()
|
||||||
|
{
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
static isDebugFilePath( filePath:string ):boolean
|
||||||
|
{
|
||||||
|
return filePath.endsWith( this.debugOutputSuffix );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async hash( data:string ):Promise<string>
|
||||||
|
{
|
||||||
|
let hash = await bcrypt.hash( data, 10 );
|
||||||
|
return Promise.resolve( hash );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifyHash( data:string, hashed:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
let isVerified = await bcrypt.compare( data, hashed );
|
||||||
|
return Promise.resolve( isVerified );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async encrypt( data:string, settings?:CryptSettings ):Promise<string>
|
||||||
|
{
|
||||||
|
settings = settings || CryptIO._cryptSettings;
|
||||||
|
|
||||||
|
const algorithm = 'aes-192-cbc';
|
||||||
|
|
||||||
|
const key = crypto.scryptSync( settings.publicKey, 'salt', 24 );
|
||||||
|
|
||||||
|
const cipher = crypto.createCipheriv( algorithm, key, settings.iv );
|
||||||
|
|
||||||
|
let encrypted = cipher.update( data, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
return Promise.resolve( encrypted );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async decrypt( data:string, settings?:CryptSettings )
|
||||||
|
{
|
||||||
|
settings = settings || CryptIO._cryptSettings;
|
||||||
|
|
||||||
|
const algorithm = 'aes-192-cbc';
|
||||||
|
|
||||||
|
const key = crypto.scryptSync( settings.publicKey, 'salt', 24 );
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv( algorithm, key, settings.iv );
|
||||||
|
|
||||||
|
let decrypted = decipher.update( data, 'hex', 'utf8' );
|
||||||
|
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createWithRandomUUID<T>( parentPath:string, data:any, onUUID?:( uuid:string, data:T ) => T ):Promise<string>
|
||||||
|
{
|
||||||
|
let uuid = CryptIO.createUUID();
|
||||||
|
let fullPath = path.join( parentPath, uuid + this.encryptionSuffix );
|
||||||
|
|
||||||
|
let pathExists = await Files.exists( fullPath );
|
||||||
|
|
||||||
|
while ( pathExists )
|
||||||
|
{
|
||||||
|
uuid = CryptIO.createUUID();
|
||||||
|
fullPath = path.join( parentPath, uuid + this.encryptionSuffix );
|
||||||
|
pathExists = await Files.exists( fullPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( onUUID )
|
||||||
|
{
|
||||||
|
data = onUUID( uuid, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
await CryptIO.saveJSON( fullPath, data );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return Promise.resolve( uuid );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadWithRandomUUID<T>( parentPath:string, uuid:string ):Promise<T>
|
||||||
|
{
|
||||||
|
let fullPath = path.join( parentPath, uuid + this.encryptionSuffix );
|
||||||
|
|
||||||
|
let fileExsits = await Files.exists( fullPath );
|
||||||
|
|
||||||
|
if ( ! fileExsits )
|
||||||
|
{
|
||||||
|
return Promise.resolve( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await this.loadJSON<T>( fullPath );
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteWithRandomUUID( parentPath:string, uuid:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
let fullPath = path.join( parentPath, uuid + this.encryptionSuffix );
|
||||||
|
|
||||||
|
let exists = await Files.exists( fullPath );
|
||||||
|
|
||||||
|
if ( ! exists )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
await Files.deleteFile( fullPath );
|
||||||
|
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateWithRandomUUID( parentPath:string, uuid:string, data:any ):Promise<void>
|
||||||
|
{
|
||||||
|
let fullPath = path.join( parentPath, uuid + this.encryptionSuffix );
|
||||||
|
await this.saveJSON( fullPath, data );
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveJSON( path:string, data:any ):Promise<void>
|
||||||
|
{
|
||||||
|
let jsonString = JSON.stringify( data );
|
||||||
|
|
||||||
|
RJLog.log( "Saving ", data, ">>", jsonString, );
|
||||||
|
|
||||||
|
let container:CryptContainer =
|
||||||
|
{
|
||||||
|
key:"",
|
||||||
|
data: await this.encrypt( jsonString )
|
||||||
|
};
|
||||||
|
|
||||||
|
let containerString = await this.encrypt( JSON.stringify( container ) );
|
||||||
|
|
||||||
|
await fs.writeFile( path, containerString );
|
||||||
|
|
||||||
|
if ( this.debugOutput )
|
||||||
|
{
|
||||||
|
await fs.writeFile( path + this.debugOutputSuffix, jsonString );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveUTF8( path:string, jsonString:string ):Promise<void>
|
||||||
|
{
|
||||||
|
RJLog.log( "Saving ", jsonString, );
|
||||||
|
|
||||||
|
let container:CryptContainer =
|
||||||
|
{
|
||||||
|
key:"",
|
||||||
|
data: await this.encrypt( jsonString )
|
||||||
|
};
|
||||||
|
|
||||||
|
let containerString = await this.encrypt( JSON.stringify( container ) );
|
||||||
|
|
||||||
|
await fs.writeFile( path, containerString );
|
||||||
|
|
||||||
|
if ( this.debugOutput )
|
||||||
|
{
|
||||||
|
await fs.writeFile( path + this.debugOutputSuffix, jsonString );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static async loadJSON<T>( path:string ):Promise<T>
|
||||||
|
{
|
||||||
|
let encryptedContainerString = await fs.readFile( path );
|
||||||
|
let containerString = await this.decrypt( encryptedContainerString.toString() );
|
||||||
|
|
||||||
|
let container = JSON.parse( containerString ) as CryptContainer;
|
||||||
|
let encryptedData = container.data;
|
||||||
|
let jsonString = await this.decrypt( encryptedData );
|
||||||
|
|
||||||
|
let json = JSON.parse( jsonString ) as T;
|
||||||
|
|
||||||
|
return Promise.resolve( json );
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadUTF8( path:string ):Promise<string>
|
||||||
|
{
|
||||||
|
let encryptedContainerString = await fs.readFile( path );
|
||||||
|
let containerString = await this.decrypt( encryptedContainerString.toString() );
|
||||||
|
|
||||||
|
let container = JSON.parse( containerString ) as CryptContainer;
|
||||||
|
let encryptedData = container.data;
|
||||||
|
let jsonString = await this.decrypt( encryptedData );
|
||||||
|
|
||||||
|
return Promise.resolve( jsonString );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
export class CryptSettings
|
||||||
|
{
|
||||||
|
publicKey:Buffer;
|
||||||
|
iv:Buffer;
|
||||||
|
|
||||||
|
static from( serializedSettings:SerializedCryptSettings )
|
||||||
|
{
|
||||||
|
let settings = new CryptSettings();
|
||||||
|
|
||||||
|
settings.iv = Buffer.alloc( serializedSettings.iv.length );
|
||||||
|
settings.publicKey = Buffer.alloc( serializedSettings.publicKey.length );
|
||||||
|
|
||||||
|
|
||||||
|
for ( let i = 0; i < serializedSettings.publicKey.length; i++ )
|
||||||
|
{
|
||||||
|
settings.publicKey[ i ] = serializedSettings.publicKey[ i ]
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( let i = 0; i < serializedSettings.iv.length; i++ )
|
||||||
|
{
|
||||||
|
settings.iv[ i ] = serializedSettings.iv[ i ]
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SerializedCryptSettings
|
||||||
|
{
|
||||||
|
publicKey:number[];
|
||||||
|
iv:number[];
|
||||||
|
|
||||||
|
static from( cryptSettings:CryptSettings )
|
||||||
|
{
|
||||||
|
let serialized = new SerializedCryptSettings();
|
||||||
|
serialized.iv = [];
|
||||||
|
serialized.publicKey = [];
|
||||||
|
|
||||||
|
for ( let i = 0; i < cryptSettings.publicKey.byteLength; i++ )
|
||||||
|
{
|
||||||
|
let value = cryptSettings.publicKey[ i ]
|
||||||
|
serialized.publicKey.push( value );
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( let i = 0; i < cryptSettings.iv.byteLength; i++ )
|
||||||
|
{
|
||||||
|
let value = cryptSettings.iv[ i ]
|
||||||
|
serialized.iv.push( value );
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,11 +8,53 @@ import { DateMath } from "../../browser/date/DateMath";
|
||||||
|
|
||||||
export class Files
|
export class Files
|
||||||
{
|
{
|
||||||
|
static escapePathFragment( unescapedPath: string, replacementCharacter: string = "_" ): string
|
||||||
|
{
|
||||||
|
|
||||||
|
let invalidChars = /[\\/:*?"<>|]/g;
|
||||||
|
|
||||||
|
let safe = unescapedPath.replace( invalidChars, replacementCharacter )
|
||||||
|
|
||||||
|
let reserved = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||||
|
|
||||||
|
if ( reserved.test( safe ) )
|
||||||
|
{
|
||||||
|
safe = `_${safe}_`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
|
|
||||||
static parentPath( filePath:string )
|
static parentPath( filePath:string )
|
||||||
{
|
{
|
||||||
return path.dirname( filePath );
|
return path.dirname( filePath );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async copy( from:string, to:string )
|
||||||
|
{
|
||||||
|
let stat = await fs.stat(from);
|
||||||
|
|
||||||
|
if ( stat.isDirectory() )
|
||||||
|
{
|
||||||
|
await fs.mkdir( to, { recursive: true } );
|
||||||
|
|
||||||
|
let entries = await fs.readdir( from );
|
||||||
|
|
||||||
|
for ( let entry of entries)
|
||||||
|
{
|
||||||
|
let srcPath = path.join( from, entry );
|
||||||
|
let destPath = path.join( to, entry );
|
||||||
|
|
||||||
|
await Files.copy(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await fs.copyFile(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static async forAllIn( filePath:string, filter:(p:PathReference)=>Promise<boolean> = null, action:(p:PathReference)=>Promise<void> = null ):Promise<PathReference[]>
|
static async forAllIn( filePath:string, filter:(p:PathReference)=>Promise<boolean> = null, action:(p:PathReference)=>Promise<void> = null ):Promise<PathReference[]>
|
||||||
{
|
{
|
||||||
let files = await fs.readdir( filePath );
|
let files = await fs.readdir( filePath );
|
||||||
|
|
@ -217,7 +259,7 @@ export class Files
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.mkdir( path );
|
await fs.mkdir( path, { recursive:true } );
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -309,11 +351,11 @@ export class Files
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async saveJSON<T>( filePath:string, data:T ):Promise<boolean>
|
static async saveJSON<T>( filePath:string, data:T, pretty:boolean = true ):Promise<boolean>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
let jsonData = JSON.stringify( data );
|
let jsonData = pretty ? JSON.stringify( data, null, " " ) : JSON.stringify( data );
|
||||||
let result = await Files.saveUTF8( filePath, jsonData );
|
let result = await Files.saveUTF8( filePath, jsonData );
|
||||||
|
|
||||||
return Promise.resolve( result );
|
return Promise.resolve( result );
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,11 @@ export class PathReference
|
||||||
return Files.loadUTF8( this.absolutePath );
|
return Files.loadUTF8( this.absolutePath );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveUTF8( text:string )
|
||||||
|
{
|
||||||
|
return Files.saveUTF8( this.absolutePath, text );
|
||||||
|
}
|
||||||
|
|
||||||
async loadHTML()
|
async loadHTML()
|
||||||
{
|
{
|
||||||
return Files.loadHTML( this.absolutePath );
|
return Files.loadHTML( this.absolutePath );
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
|
||||||
|
import { RJLog } from "../log/RJLog";
|
||||||
|
import { RequestRequirement } from "./requirements/RequestRequirement";
|
||||||
|
import { UserManagementServer } from "./UserManagementServer";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { Message } from "../../browser/messages/Message";
|
||||||
|
import { UserData } from "./UserData";
|
||||||
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
import { LocationData } from "./location/LocationData";
|
||||||
|
|
||||||
|
export enum RequestType
|
||||||
|
{
|
||||||
|
GET,
|
||||||
|
POST
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class RequestHandler
|
||||||
|
{
|
||||||
|
|
||||||
|
_ums:UserManagementServer;
|
||||||
|
_type:RequestType;
|
||||||
|
_url:string;
|
||||||
|
|
||||||
|
|
||||||
|
requirements:RequestRequirement[] = [];
|
||||||
|
|
||||||
|
get app(){ return this._ums.app };
|
||||||
|
get userDB(){ return this._ums.userDB; }
|
||||||
|
|
||||||
|
constructor( rt:RequestType, url:string, requirements:RequestRequirement[] = [] )
|
||||||
|
{
|
||||||
|
this._url = url;
|
||||||
|
this._type = rt;
|
||||||
|
this.requirements = requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async initialize( ums:UserManagementServer ):Promise<void>
|
||||||
|
{
|
||||||
|
this._ums = ums;
|
||||||
|
|
||||||
|
await this._initialize();
|
||||||
|
|
||||||
|
await this._register();
|
||||||
|
|
||||||
|
for ( let r of this.requirements )
|
||||||
|
{
|
||||||
|
await r.initialize( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _register():Promise<void>
|
||||||
|
{
|
||||||
|
RJLog.log( RequestType[ this._type ], this._url );
|
||||||
|
|
||||||
|
if ( RequestType.GET == this._type )
|
||||||
|
{
|
||||||
|
this.app.get( this._url, ( request, reply ) => { this.handle( request, reply ); } );
|
||||||
|
}
|
||||||
|
else if ( RequestType.POST == this._type )
|
||||||
|
{
|
||||||
|
this.app.post( this._url, ( request, reply ) => { this.handle( request, reply ); } );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _currentRequest:FastifyRequest;
|
||||||
|
protected _currentReply:FastifyReply;
|
||||||
|
|
||||||
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<void>
|
||||||
|
{
|
||||||
|
if ( ! request || ! reply )
|
||||||
|
{
|
||||||
|
RJLog.warn( "Aborting request:", "Request:", ! request, "Reply:", ! reply );
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentRequest = request;
|
||||||
|
this._currentReply = reply;
|
||||||
|
|
||||||
|
RJLog.log( "Processing request:", "Request:", request.url, request.ip );
|
||||||
|
|
||||||
|
for ( let r of this._ums.globalRequirements )
|
||||||
|
{
|
||||||
|
let fullfilled = await r.handle( request, reply );
|
||||||
|
|
||||||
|
if ( ! fullfilled )
|
||||||
|
{
|
||||||
|
RJLog.log( "Global Requirement not fullfilled", r );
|
||||||
|
|
||||||
|
return this.sendError( "Error during global requirements check" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( let r of this.requirements )
|
||||||
|
{
|
||||||
|
let fullfilled = await r.handle( request, reply );
|
||||||
|
|
||||||
|
if ( ! fullfilled )
|
||||||
|
{
|
||||||
|
RJLog.log( "Requirement not fullfilled", r );
|
||||||
|
|
||||||
|
return this.sendError( "Error during requirements check" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await this._handle( request, reply );
|
||||||
|
|
||||||
|
this._currentRequest = null;
|
||||||
|
this._currentReply = null;
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _initialize():Promise<void>
|
||||||
|
{
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
protected abstract _handle( request:FastifyRequest, reply:FastifyReply ):Promise<void>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
get ip()
|
||||||
|
{
|
||||||
|
return this._currentRequest.ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get userAgent()
|
||||||
|
{
|
||||||
|
return this._currentRequest.headers[ "user-agent" ];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocation():Promise<LocationData>
|
||||||
|
{
|
||||||
|
return this._ums.location.getLocation( this.ip );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendJSON( obj:any ):Promise<void>
|
||||||
|
{
|
||||||
|
this._currentReply.send( obj );
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser():Promise<UserData>
|
||||||
|
{
|
||||||
|
let request = this._currentRequest;
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let tokenData = requestBody as { token:string };
|
||||||
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
|
let session = this._ums._sessions.get( tokenID );
|
||||||
|
return this._ums.userDB.byID( session.userID );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendInfo( info:string ):Promise<void>
|
||||||
|
{
|
||||||
|
RJLog.log( info );
|
||||||
|
return this.sendJSON( Message.Info( info ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendError( error:string, with400ErrorCode:boolean = true ):Promise<void>
|
||||||
|
{
|
||||||
|
RJLog.log( error );
|
||||||
|
|
||||||
|
if ( with400ErrorCode )
|
||||||
|
{
|
||||||
|
this._currentReply.code( 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sendJSON( Message.Error( error ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendEmail( to:string, title:string, message:string )
|
||||||
|
{
|
||||||
|
this._ums.sendEmail( to, title, message );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendDataInfo( info:string, data:any )
|
||||||
|
{
|
||||||
|
let json = Message.Info( info ) as any;
|
||||||
|
json.data = data;
|
||||||
|
return this.sendJSON( json );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class Session
|
||||||
|
{
|
||||||
|
userID:string;
|
||||||
|
token:string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ISOTimeStamp } from "../../browser/date/ISOTimeStamp";
|
||||||
|
|
||||||
|
export class Token
|
||||||
|
{
|
||||||
|
id:string;
|
||||||
|
expires:ISOTimeStamp;
|
||||||
|
hashedIP:string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { DateHelper } from "../../browser/date/DateHelper";
|
||||||
|
import { DateMath } from "../../browser/date/DateMath";
|
||||||
|
import { ISOTimeStamp } from "../../browser/date/ISOTimeStamp";
|
||||||
|
import { CryptIO } from "../crypt/CryptIO";
|
||||||
|
import { Token } from "./Token";
|
||||||
|
import { UserManagementServer } from "./UserManagementServer";
|
||||||
|
|
||||||
|
export class TokenDB
|
||||||
|
{
|
||||||
|
static _defaultExpirationDurationMinutes = 20;
|
||||||
|
|
||||||
|
_ums:UserManagementServer;
|
||||||
|
_tokens = new Map<string,Token>();
|
||||||
|
|
||||||
|
constructor( ums:UserManagementServer )
|
||||||
|
{
|
||||||
|
this._ums = ums;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async create( ip:string, expireDurationInMinutes:number = -1 ):Promise<Token>
|
||||||
|
{
|
||||||
|
expireDurationInMinutes = expireDurationInMinutes == -1 ? TokenDB._defaultExpirationDurationMinutes : expireDurationInMinutes;
|
||||||
|
|
||||||
|
let token = new Token();
|
||||||
|
token.id = CryptIO.createUUID();
|
||||||
|
token.hashedIP = await CryptIO.hash( ip );
|
||||||
|
token.expires = ISOTimeStamp.fromDate( DateMath.fromNowAddMinutes( expireDurationInMinutes ) );
|
||||||
|
|
||||||
|
this._tokens.set( token.id, token );
|
||||||
|
|
||||||
|
return Promise.resolve( token );
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate( token:Token, ip:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
if ( this._tokens.get( token.id ) != token )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( DateMath.isExpired( ISOTimeStamp.toDate( token.expires ) ) )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let isVerified = await CryptIO.verifyHash( ip, token.hashedIP );
|
||||||
|
|
||||||
|
if ( isVerified )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { Arrays } from "../../browser/tools/Arrays";
|
||||||
|
import { CryptIO } from "../crypt/CryptIO";
|
||||||
|
import { Files } from "../files/Files";
|
||||||
|
import { Permission } from "./permissions/Permission";
|
||||||
|
import { Role } from "./permissions/Role";
|
||||||
|
import { Token } from "./Token";
|
||||||
|
import { TokenDB } from "./TokenDB";
|
||||||
|
import { UserData } from "./UserData";
|
||||||
|
import { UserManagementServer } from "./UserManagementServer";
|
||||||
|
|
||||||
|
export class SerializedUserDB
|
||||||
|
{
|
||||||
|
users:UserData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserDB
|
||||||
|
{
|
||||||
|
_ums:UserManagementServer;
|
||||||
|
_path:string;
|
||||||
|
|
||||||
|
users:UserData[] = [];
|
||||||
|
_pendingUsers:UserData[] = [];
|
||||||
|
|
||||||
|
_signUpTokens = new Map<string,Token>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async hasUserWithEmail( email:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
return this.users.findIndex( u => u.email === email ) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUp( email:string, password:string, userName:string = "User" ):Promise<UserData>
|
||||||
|
{
|
||||||
|
let userData = this._pendingUsers.find( u => u.email === email );
|
||||||
|
|
||||||
|
if ( ! userData )
|
||||||
|
{
|
||||||
|
userData = new UserData();
|
||||||
|
userData.id = CryptIO.createUUID();
|
||||||
|
userData.email = email;
|
||||||
|
userData.hashedPassword = await CryptIO.hash( password );
|
||||||
|
userData.name = userName;
|
||||||
|
|
||||||
|
this._pendingUsers.push( userData );
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Promise.resolve( userData );
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSignUpConfirmation( userData:UserData, ip:string ):Promise<Token>
|
||||||
|
{
|
||||||
|
let token = await this._ums.tokenDB.create( ip );
|
||||||
|
|
||||||
|
this._signUpTokens.set( userData.id, token );
|
||||||
|
|
||||||
|
return Promise.resolve( token );
|
||||||
|
}
|
||||||
|
|
||||||
|
async byID( id:string )
|
||||||
|
{
|
||||||
|
return this.users.find( u => u.id === id );
|
||||||
|
}
|
||||||
|
|
||||||
|
async byEmail( email:string )
|
||||||
|
{
|
||||||
|
return this.users.find( u => u.email === email );
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmUserSignUp( userID:string, tokenID:string, ip:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
let user = this._pendingUsers.find( u => u.id == userID );
|
||||||
|
|
||||||
|
if ( ! user )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! this._signUpTokens.has( userID ) )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = this._signUpTokens.get( userID );
|
||||||
|
|
||||||
|
if ( token.id != tokenID )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValid = await this._ums.tokenDB.validate( token, ip );
|
||||||
|
|
||||||
|
if ( isValid )
|
||||||
|
{
|
||||||
|
Arrays.remove( this._pendingUsers, user );
|
||||||
|
this.users.push( user );
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( isValid );
|
||||||
|
}
|
||||||
|
|
||||||
|
async login( email:string, password:string, ip:string ):Promise<Token>
|
||||||
|
{
|
||||||
|
let user = await this.byEmail( email );
|
||||||
|
|
||||||
|
if ( ! user )
|
||||||
|
{
|
||||||
|
return Promise.resolve( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
let passwordValid = await CryptIO.verifyHash( password, user.hashedPassword );
|
||||||
|
|
||||||
|
if ( ! passwordValid )
|
||||||
|
{
|
||||||
|
return Promise.resolve( null );
|
||||||
|
}
|
||||||
|
|
||||||
|
let hour = 60;
|
||||||
|
let day = hour * 24;
|
||||||
|
let week = day * 7;
|
||||||
|
|
||||||
|
let token = await this._ums.tokenDB.create( ip, week );
|
||||||
|
|
||||||
|
return Promise.resolve( token );
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasPermission( ud:UserData, permissionID:string )
|
||||||
|
{
|
||||||
|
let hasOwnPermission = await this._hasPermission( permissionID, ud.permissions );
|
||||||
|
|
||||||
|
if ( hasOwnPermission )
|
||||||
|
{
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
let roleID = ud.role;
|
||||||
|
|
||||||
|
while ( roleID )
|
||||||
|
{
|
||||||
|
let role = this._ums._roles.get( roleID );
|
||||||
|
|
||||||
|
if ( ! role )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasRolePermission = await this._hasPermission( permissionID, role.permissions );
|
||||||
|
|
||||||
|
if ( hasRolePermission )
|
||||||
|
{
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
roleID = role.inherits;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _hasPermission( permissionID:string, permissions:Permission[] ):Promise<boolean>
|
||||||
|
{
|
||||||
|
for ( let p of permissions )
|
||||||
|
{
|
||||||
|
if ( Permission.isMatching( permissionID, p ) )
|
||||||
|
{
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
async save()
|
||||||
|
{
|
||||||
|
let serializedDB = new SerializedUserDB();
|
||||||
|
serializedDB.users = this.users;
|
||||||
|
|
||||||
|
await Files.saveJSON( this._path, serializedDB );
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async load( ums:UserManagementServer, path:string ):Promise<UserDB>
|
||||||
|
{
|
||||||
|
let userDB = new UserDB();
|
||||||
|
userDB._ums = ums;
|
||||||
|
userDB._path = path;
|
||||||
|
|
||||||
|
let userDBExists = await Files.exists( userDB._path );
|
||||||
|
|
||||||
|
if ( userDBExists )
|
||||||
|
{
|
||||||
|
let data = await Files.loadJSON<SerializedUserDB>( userDB._path );
|
||||||
|
userDB.users = data.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Promise.resolve( userDB );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { UAParser } from "ua-parser-js";
|
||||||
|
import { ISOTimeStamp } from "../../browser/date/ISOTimeStamp";
|
||||||
|
import { Permission } from "./permissions/Permission";
|
||||||
|
import { Role } from "./permissions/Role";
|
||||||
|
import { LocationData } from "./location/LocationData";
|
||||||
|
|
||||||
|
|
||||||
|
export class UserLoginData
|
||||||
|
{
|
||||||
|
timeStamp:ISOTimeStamp;
|
||||||
|
location:LocationData;
|
||||||
|
userAgent:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserData
|
||||||
|
{
|
||||||
|
id:string;
|
||||||
|
email:string;
|
||||||
|
hashedPassword:string;
|
||||||
|
name:string;
|
||||||
|
lastLogins:UserLoginData[];
|
||||||
|
role:string;
|
||||||
|
permissions:Permission[];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
import Fastify, { FastifyInstance } from "fastify";
|
||||||
|
import fastifyMultipart from "@fastify/multipart";
|
||||||
|
import cors from '@fastify/cors';
|
||||||
|
|
||||||
|
import { UserDB } from "./UserDB";
|
||||||
|
import { RequestHandler } from "./RequestHandler";
|
||||||
|
import { EmailService as EmailService } from "./email/EmailService";
|
||||||
|
import { TokenDB } from "./TokenDB";
|
||||||
|
import { UserManagementServerSettings } from "./UserManagementServerSettings";
|
||||||
|
import { RJLog } from "../log/RJLog";
|
||||||
|
import { Session } from "./Session";
|
||||||
|
import { Role } from "./permissions/Role";
|
||||||
|
import { RolesData } from "./permissions/RolesData";
|
||||||
|
import { Files } from "../files/Files";
|
||||||
|
import { LocationService } from "./location/LocationService";
|
||||||
|
import { RequestRequirement } from "./requirements/RequestRequirement";
|
||||||
|
import { NotTooManyRequests } from "./requirements/security/NotTooManyRequests";
|
||||||
|
|
||||||
|
export class UserManagementServer
|
||||||
|
{
|
||||||
|
_app:FastifyInstance;
|
||||||
|
get app(){ return this._app; }
|
||||||
|
|
||||||
|
_userDB:UserDB;
|
||||||
|
get userDB(){ return this._userDB; }
|
||||||
|
|
||||||
|
_tokenDB:TokenDB;
|
||||||
|
get tokenDB(){ return this._tokenDB;}
|
||||||
|
|
||||||
|
_emailService:EmailService;
|
||||||
|
get email(){ return this._emailService; }
|
||||||
|
|
||||||
|
_locationService:LocationService;
|
||||||
|
get location(){ return this._locationService; }
|
||||||
|
|
||||||
|
_sessions:Map<string,Session> = new Map<string,Session>();
|
||||||
|
_roles = new Map<string,Role>();
|
||||||
|
|
||||||
|
_handlers:RequestHandler[] = [];
|
||||||
|
|
||||||
|
_settings:UserManagementServerSettings;
|
||||||
|
_globalRequirements:RequestRequirement[] = [];
|
||||||
|
get globalRequirements(){ return this._globalRequirements; }
|
||||||
|
|
||||||
|
|
||||||
|
async initialize( settings:UserManagementServerSettings, mailService:EmailService, handlers:RequestHandler[], globalRequirements:RequestRequirement[] = null ):Promise<void>
|
||||||
|
{
|
||||||
|
this._settings = settings;
|
||||||
|
|
||||||
|
UserManagementServerSettings.makeAllPathsAbsolute( settings );
|
||||||
|
|
||||||
|
await this._initializeApp();
|
||||||
|
await this._addGlobalRequirements( globalRequirements || UserManagementServer.DefaultGlobalRequirements() );
|
||||||
|
await this._addServices( mailService );
|
||||||
|
await this._addHandlers( handlers );
|
||||||
|
|
||||||
|
await this._startServer();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static DefaultGlobalRequirements():RequestRequirement[]
|
||||||
|
{
|
||||||
|
let globalDefaults:RequestRequirement[] =
|
||||||
|
[
|
||||||
|
new NotTooManyRequests()
|
||||||
|
];
|
||||||
|
|
||||||
|
return globalDefaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addGlobalRequirements( globalRequirements:RequestRequirement[] ):Promise<void>
|
||||||
|
{
|
||||||
|
this._globalRequirements = globalRequirements;
|
||||||
|
|
||||||
|
for ( let gr of this._globalRequirements )
|
||||||
|
{
|
||||||
|
await gr.initializeGlobal( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmail( to:string, title:string, message:string ):Promise<void>
|
||||||
|
{
|
||||||
|
return this.email.send( this._settings.emailFrom, to, title, message );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initializeApp():Promise<void>
|
||||||
|
{
|
||||||
|
|
||||||
|
this._app = Fastify();
|
||||||
|
this._app.register( fastifyMultipart );
|
||||||
|
|
||||||
|
for ( let corsURL of this._settings.corsURLs )
|
||||||
|
{
|
||||||
|
RJLog.log( "Adding cors:", corsURL );
|
||||||
|
|
||||||
|
await this._app.register( cors,
|
||||||
|
{
|
||||||
|
origin: corsURL,
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addServices( mailService:EmailService ):Promise<void>
|
||||||
|
{
|
||||||
|
this._userDB = await UserDB.load( this, this._settings.userDBPath );
|
||||||
|
|
||||||
|
let rolesData = await Files.loadJSON<RolesData>( this._settings.rolesPath );
|
||||||
|
this._roles = new Map<string,Role>();
|
||||||
|
rolesData.roles.forEach( r => this._roles.set( r.id, r ) );
|
||||||
|
|
||||||
|
this._tokenDB = new TokenDB( this );
|
||||||
|
this._emailService = mailService;
|
||||||
|
|
||||||
|
this._locationService = await LocationService.create(
|
||||||
|
this._settings.geoLocationPath,
|
||||||
|
this._settings.geoAccountID,
|
||||||
|
this._settings.geoLicenseKey
|
||||||
|
);
|
||||||
|
|
||||||
|
let ipToCheck = "2a02:3100:25e5:2500:65d7:61b7:33f7:9d7f";
|
||||||
|
let location = await this._locationService.getLocation( ipToCheck );
|
||||||
|
|
||||||
|
RJLog.log( "IP", ipToCheck, "Location:", location );
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addHandlers( handlers:RequestHandler[] ):Promise<void>
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
for ( let handler of handlers )
|
||||||
|
{
|
||||||
|
await handler.initialize( this );
|
||||||
|
this._handlers.push( handler );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _startServer()
|
||||||
|
{
|
||||||
|
this.app.listen(
|
||||||
|
|
||||||
|
{ port: this._settings.port },
|
||||||
|
|
||||||
|
( error, address ) =>
|
||||||
|
{
|
||||||
|
if ( error )
|
||||||
|
{
|
||||||
|
RJLog.error( error );
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
RJLog.log( `Server running on ${address}` );
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { RegExpUtility } from "../../browser/text/RegExpUtitlity";
|
||||||
|
|
||||||
|
export class UserManagementServerSettings
|
||||||
|
{
|
||||||
|
// Server settings
|
||||||
|
url:string;
|
||||||
|
port:number = 8084;
|
||||||
|
corsURLs:string[] = [];
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
rootPath:string;
|
||||||
|
userDBPath:string;
|
||||||
|
rolesPath:string;
|
||||||
|
|
||||||
|
// Geo-Lite Lib
|
||||||
|
geoLocationPath:string;
|
||||||
|
geoAccountID:string;
|
||||||
|
geoLicenseKey:string;
|
||||||
|
|
||||||
|
// Email
|
||||||
|
emailFrom:string;
|
||||||
|
|
||||||
|
// User settings
|
||||||
|
maxLogins:number = 5;
|
||||||
|
|
||||||
|
|
||||||
|
static makeAllPathsAbsolute( settings:UserManagementServerSettings )
|
||||||
|
{
|
||||||
|
settings.userDBPath = this._makePathAbsolute( settings.userDBPath, settings.rootPath );
|
||||||
|
settings.rolesPath = this._makePathAbsolute( settings.rolesPath, settings.rootPath );
|
||||||
|
settings.geoLocationPath = this._makePathAbsolute( settings.geoLocationPath, settings.rootPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static _makePathAbsolute( path:string, rootPath:string )
|
||||||
|
{
|
||||||
|
if ( path.indexOf( rootPath ) === 0 )
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RegExpUtility.joinPaths( [ rootPath, path ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { DateHelper } from "../../../browser/date/DateHelper";
|
||||||
|
import { DateFormatter } from "../../../browser/date/DateFormatter";
|
||||||
|
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp";
|
||||||
|
import { Files } from "../../files/Files";
|
||||||
|
import { PathReference } from "../../files/PathReference";
|
||||||
|
import { EmailService } from "./EmailService";
|
||||||
|
|
||||||
|
export class DebugEmail extends EmailService
|
||||||
|
{
|
||||||
|
path:string;
|
||||||
|
|
||||||
|
async send( from:string, to:string, title:string, message:string )
|
||||||
|
{
|
||||||
|
let userName = Files.escapePathFragment( to );
|
||||||
|
let dateInfo = DateFormatter.YMD_HMS( DateHelper.now() );
|
||||||
|
let emailFileName = Files.escapePathFragment( dateInfo + " - " + title );
|
||||||
|
|
||||||
|
let userPath = new PathReference( this.path ).createRelative( userName );
|
||||||
|
|
||||||
|
let titlePath = userPath.createRelative( emailFileName + ".html");
|
||||||
|
|
||||||
|
await Files.ensureParentDirectoryExists( titlePath.absolutePath );
|
||||||
|
|
||||||
|
titlePath.saveUTF8( message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export abstract class EmailService
|
||||||
|
{
|
||||||
|
abstract send( from:string, to:string, title:string, message:string ):Promise<void>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { CryptIO } from "../../crypt/CryptIO";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
import { RequestHandler, RequestType } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
|
||||||
|
export class ConfirmSignUpHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/confirm-signup";
|
||||||
|
constructor(){ super( RequestType.GET, ConfirmSignUpHandler.url ); }
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
const { id, token } = request.query as { id?: string; token?: string };
|
||||||
|
|
||||||
|
if ( ! id || ! token )
|
||||||
|
{
|
||||||
|
return this.sendError( "Missing id or token" );
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await this.userDB.confirmUserSignUp( id, token, this.ip );
|
||||||
|
|
||||||
|
if ( ! result )
|
||||||
|
{
|
||||||
|
return this.sendError( "User signup confimration failed" );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sendInfo( "User creation confirmed" );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity";
|
||||||
|
import { CryptIO } from "../../crypt/CryptIO";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
import { RequestHandler, RequestType } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer";
|
||||||
|
import { ConfirmSignUpHandler } from "./confirm-signup";
|
||||||
|
import { Session } from "../Session";
|
||||||
|
import { LocationData } from "../location/LocationData";
|
||||||
|
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp";
|
||||||
|
import { DateFormatter } from "../../../browser/date/DateFormatter";
|
||||||
|
import { UAParser } from "ua-parser-js";
|
||||||
|
import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn";
|
||||||
|
|
||||||
|
export class UserLoginDataInfo
|
||||||
|
{
|
||||||
|
time:string;
|
||||||
|
location:LocationData;
|
||||||
|
app:string;
|
||||||
|
os:string;
|
||||||
|
deviceType:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class InfoHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/info";
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super( RequestType.POST, InfoHandler.url, [ new UserIsLoggedIn() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let tokenData = requestBody as { token:string };
|
||||||
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
|
let session = this._ums._sessions.get( tokenID );
|
||||||
|
let user = await this._ums.userDB.byID( session.userID );
|
||||||
|
|
||||||
|
let lastLogins = user.lastLogins.map(
|
||||||
|
( ld )=>
|
||||||
|
{
|
||||||
|
let ui = new UserLoginDataInfo();
|
||||||
|
ui.time = DateFormatter.forUsers( ISOTimeStamp.toDate( ld.timeStamp ) );
|
||||||
|
ui.location = ld.location;
|
||||||
|
|
||||||
|
let parser = new UAParser( ld.userAgent );
|
||||||
|
let result = parser.getResult();
|
||||||
|
ui.app = result?.browser?.name || "-";
|
||||||
|
ui.os = result?.os?.name || "-";
|
||||||
|
ui.deviceType = result?.device?.type || "desktop";
|
||||||
|
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let info = { name:user.name, email:user.email, lastLogins:lastLogins };
|
||||||
|
|
||||||
|
return this.sendDataInfo( "User Info", info );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity";
|
||||||
|
import { CryptIO } from "../../crypt/CryptIO";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
import { RequestHandler, RequestType } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer";
|
||||||
|
import { ConfirmSignUpHandler } from "./confirm-signup";
|
||||||
|
import { Session } from "../Session";
|
||||||
|
import { UserLoginData } from "../UserData";
|
||||||
|
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp";
|
||||||
|
import { Arrays } from "../../../browser/tools/Arrays";
|
||||||
|
|
||||||
|
export class LoginHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/login";
|
||||||
|
constructor(){ super( RequestType.POST, LoginHandler.url ); }
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let { email, password, userName } = requestBody as { email: string; password: string; userName: string };
|
||||||
|
|
||||||
|
if ( ! email || ! password )
|
||||||
|
{
|
||||||
|
return this.sendError( "Missing email or password:" + `"${requestBody}"` );
|
||||||
|
}
|
||||||
|
|
||||||
|
let loginToken = await this.userDB.login( email, password, this.ip );
|
||||||
|
|
||||||
|
if ( ! loginToken )
|
||||||
|
{
|
||||||
|
return this.sendError( "Login failed" );
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.userDB.byEmail( email );
|
||||||
|
|
||||||
|
let loginData = new UserLoginData();
|
||||||
|
loginData.timeStamp = ISOTimeStamp.now();
|
||||||
|
loginData.location = await this.getLocation();
|
||||||
|
loginData.userAgent = this.userAgent;
|
||||||
|
|
||||||
|
user.lastLogins = user.lastLogins || [];
|
||||||
|
user.lastLogins.push( loginData );
|
||||||
|
|
||||||
|
Arrays.shiftToSize( user.lastLogins, this._ums._settings.maxLogins );
|
||||||
|
|
||||||
|
|
||||||
|
let session = new Session();
|
||||||
|
session.token = loginToken.id;
|
||||||
|
session.userID = user.id;
|
||||||
|
|
||||||
|
this._ums._sessions.set( session.token, session );
|
||||||
|
|
||||||
|
await this.userDB.save();
|
||||||
|
|
||||||
|
|
||||||
|
return this.sendDataInfo( "Login successfull", { token: session.token } );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity";
|
||||||
|
import { CryptIO } from "../../crypt/CryptIO";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
import { RequestHandler, RequestType } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer";
|
||||||
|
import { ConfirmSignUpHandler } from "./confirm-signup";
|
||||||
|
import { Session } from "../Session";
|
||||||
|
import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn";
|
||||||
|
|
||||||
|
export class LogoutHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/logout";
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super( RequestType.POST, LogoutHandler.url, [ new UserIsLoggedIn() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let tokenData = requestBody as { token:string };
|
||||||
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
|
this._ums._sessions.delete( tokenID );
|
||||||
|
this._ums.tokenDB._tokens.delete( tokenID );
|
||||||
|
|
||||||
|
return this.sendInfo( "Logout successfull" );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity";
|
||||||
|
import { CryptIO } from "../../crypt/CryptIO";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
import { RequestHandler, RequestType } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer";
|
||||||
|
import { ConfirmSignUpHandler } from "./confirm-signup";
|
||||||
|
|
||||||
|
export class SignUpHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/signup";
|
||||||
|
constructor(){ super( RequestType.POST, SignUpHandler.url ); }
|
||||||
|
|
||||||
|
static USER_NAME = "USER_NAME";
|
||||||
|
static CONFIRMATION_LINK = "CONFIRMATION_LINK";
|
||||||
|
|
||||||
|
static EMAIL_TITLE = "You signed up";
|
||||||
|
static EMAIL_MESSAGE =
|
||||||
|
`
|
||||||
|
<b>Hi {{${SignUpHandler.USER_NAME}}}!</b>
|
||||||
|
<br>
|
||||||
|
Please confirm your signup by clicking the link:
|
||||||
|
<br>
|
||||||
|
<a href="{{${SignUpHandler.CONFIRMATION_LINK}}}">SIGN UP CONFIRMATION</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Cheers!
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let { email, password, userName } = requestBody as { email: string; password: string; userName: string };
|
||||||
|
|
||||||
|
if ( ! email || ! password )
|
||||||
|
{
|
||||||
|
return this.sendError( "Missing email or password:" + `"${requestBody}"` );
|
||||||
|
}
|
||||||
|
|
||||||
|
let emailInUse = await this.userDB.hasUserWithEmail( email );
|
||||||
|
|
||||||
|
if ( emailInUse )
|
||||||
|
{
|
||||||
|
return this.sendError( "User already exists" );
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData = await this.userDB.byEmail( email );
|
||||||
|
|
||||||
|
if ( userData == null )
|
||||||
|
{
|
||||||
|
userData = await this.userDB.signUp( email, password, userName );
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = await this.userDB.createSignUpConfirmation( userData, this.ip );
|
||||||
|
|
||||||
|
let confirmationURL = this._ums._settings.url + ConfirmSignUpHandler.url;
|
||||||
|
|
||||||
|
let id = userData.id;
|
||||||
|
|
||||||
|
let link = `${confirmationURL}?id=${id}&token=${token.id}`;
|
||||||
|
|
||||||
|
await this.sendEmail( email, userName, link );
|
||||||
|
|
||||||
|
|
||||||
|
return this.sendInfo( "User created, email sent" );
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmail( email:string, userName:string, link:string ):Promise<void>
|
||||||
|
{
|
||||||
|
|
||||||
|
let variables:Variables = {
|
||||||
|
[ SignUpHandler.USER_NAME ] : userName,
|
||||||
|
[ SignUpHandler.CONFIRMATION_LINK ] : link
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = VariableReplacer.replace( SignUpHandler.EMAIL_MESSAGE, variables, "{{", "}}" );
|
||||||
|
let title = VariableReplacer.replace( SignUpHandler.EMAIL_TITLE, variables, "{{", "}}" );
|
||||||
|
|
||||||
|
await this._ums.sendEmail( email,title, message );
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class LocationData
|
||||||
|
{
|
||||||
|
country:string;
|
||||||
|
city:string;
|
||||||
|
km_range:number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { Files } from "../../files/Files";
|
||||||
|
import { LocationData } from "./LocationData";
|
||||||
|
import * as https from "https";
|
||||||
|
import * as zlib from "zlib";
|
||||||
|
import * as tar from "tar";
|
||||||
|
import maxmind, { CityResponse, Reader, Response } from "maxmind";
|
||||||
|
import { RJLog } from "../../log/RJLog";
|
||||||
|
|
||||||
|
export class LocationService
|
||||||
|
{
|
||||||
|
_dbPath:string;
|
||||||
|
_accountID:string;
|
||||||
|
_licenseKey:string;
|
||||||
|
_url:string = "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz";
|
||||||
|
_lookUpDB:Reader<CityResponse>;
|
||||||
|
|
||||||
|
static async create( path:string, accountID:string, licenseKey:string, url = null ):Promise<LocationService>
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
let ipToL = new LocationService();
|
||||||
|
ipToL._dbPath = path;
|
||||||
|
ipToL._accountID = accountID;
|
||||||
|
ipToL._licenseKey = licenseKey;
|
||||||
|
|
||||||
|
if ( url )
|
||||||
|
{
|
||||||
|
ipToL._url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
RJLog.log( "Starting Location Service", ipToL._dbPath, ipToL._accountID, ipToL._licenseKey, ipToL._url );
|
||||||
|
|
||||||
|
await ipToL.update();
|
||||||
|
|
||||||
|
if ( ipToL._lookUpDB == null )
|
||||||
|
{
|
||||||
|
await ipToL._loadDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipToL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update( forceUpdate:boolean = false ):Promise<void>
|
||||||
|
{
|
||||||
|
let needsUpdate = forceUpdate || ( await this._needsUpdate() );
|
||||||
|
|
||||||
|
if ( ! needsUpdate )
|
||||||
|
{
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this._downloadDB();
|
||||||
|
await this._loadDB();
|
||||||
|
}
|
||||||
|
catch( e )
|
||||||
|
{
|
||||||
|
RJLog.error( "Updating failed:", e );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadDB()
|
||||||
|
{
|
||||||
|
this._lookUpDB = await maxmind.open( this._dbPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _needsUpdate( maxAgeDays = 7 )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
let stats = await Files.getStatistics( this._dbPath );
|
||||||
|
let ageDays = ( Date.now() - stats.mtimeMs ) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
return Promise.resolve( ageDays > maxAgeDays );
|
||||||
|
}
|
||||||
|
catch ( error )
|
||||||
|
{
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _downloadDB()
|
||||||
|
{
|
||||||
|
RJLog.log( "Downloading DB" );
|
||||||
|
|
||||||
|
|
||||||
|
await Files.ensureParentDirectoryExists( this._dbPath );
|
||||||
|
|
||||||
|
let authenticification = `${this._accountID}:${this._licenseKey}`;
|
||||||
|
|
||||||
|
let promise = new Promise<void>(
|
||||||
|
|
||||||
|
( resolve, reject ) =>
|
||||||
|
{
|
||||||
|
let sendRequest = ( url:string, writeAuthHeader:boolean ) =>
|
||||||
|
{
|
||||||
|
let options:any = {
|
||||||
|
headers:
|
||||||
|
{
|
||||||
|
"Accept": "application/gzip"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( writeAuthHeader )
|
||||||
|
{
|
||||||
|
let authHeader = "Basic " + Buffer.from( authenticification ).toString( "base64" );
|
||||||
|
|
||||||
|
options =
|
||||||
|
{
|
||||||
|
headers:
|
||||||
|
{
|
||||||
|
"Authorization": authHeader,
|
||||||
|
"Accept": "application/gzip"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RJLog.log( "Downloading at ", url, options );
|
||||||
|
|
||||||
|
https.get( url, options,
|
||||||
|
|
||||||
|
( result ) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
if ( result.statusCode === 302 || result.statusCode == 301 )
|
||||||
|
{
|
||||||
|
let redirectUrl = result.headers.location;
|
||||||
|
console.log(`Redirecting to ${redirectUrl}`);
|
||||||
|
result.resume();
|
||||||
|
return sendRequest( redirectUrl, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( result.statusCode !== 200 )
|
||||||
|
{
|
||||||
|
RJLog.log( `Location db download failed: ${result.statusCode} ${result.statusMessage}` );
|
||||||
|
reject( new Error( `Location db download failed: ${result.statusCode} ${result.statusMessage}` ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pipe through gunzip then tar extractor, writing .mmdb to DB_DIR
|
||||||
|
let gunzip = zlib.createGunzip();
|
||||||
|
let extractor = tar.x(
|
||||||
|
{
|
||||||
|
cwd: Files.parentPath( this._dbPath ),
|
||||||
|
filter: ( tarPath ) => tarPath.endsWith( ".mmdb" ),
|
||||||
|
strip: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
result.pipe( gunzip ).pipe( extractor );
|
||||||
|
|
||||||
|
extractor.on( "finish", () => resolve());
|
||||||
|
extractor.on( "error", ( error ) => reject( error ) );
|
||||||
|
result.on( "error", ( error ) => reject( error ) );
|
||||||
|
}
|
||||||
|
).on("error", (error) => reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest( this._url, true );
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLocation( ip:string ):Promise<LocationData>
|
||||||
|
{
|
||||||
|
let locationData = new LocationData();
|
||||||
|
locationData.country = "Unknown";
|
||||||
|
locationData.city = "Unknown";
|
||||||
|
locationData.km_range = 20000;
|
||||||
|
|
||||||
|
if ( this._lookUpDB !== null )
|
||||||
|
{
|
||||||
|
let geo = this._lookUpDB.get( ip );
|
||||||
|
locationData.country = geo?.country?.names?.en ?? "Unknown";
|
||||||
|
locationData.city = geo?.city?.names?.en ?? "Unknown";
|
||||||
|
locationData.km_range = geo?.location?.accuracy_radius ?? 20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { DateMath } from "../../../browser/date/DateMath";
|
||||||
|
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp";
|
||||||
|
|
||||||
|
export class Permission
|
||||||
|
{
|
||||||
|
id:string;
|
||||||
|
by:string;
|
||||||
|
granted:ISOTimeStamp;
|
||||||
|
expires:ISOTimeStamp;
|
||||||
|
revokableBy:string[];
|
||||||
|
|
||||||
|
|
||||||
|
static isMatching( permissionID:string, permission:Permission )
|
||||||
|
{
|
||||||
|
return permission.id === permissionID && Permission.isValid( permission );
|
||||||
|
}
|
||||||
|
|
||||||
|
static isValid( permission:Permission )
|
||||||
|
{
|
||||||
|
let isExpired = true;
|
||||||
|
|
||||||
|
let expiresInfo = permission.expires;
|
||||||
|
|
||||||
|
if ( ! expiresInfo || DateMath.isExpired( ISOTimeStamp.toDate( expiresInfo ) ) )
|
||||||
|
{
|
||||||
|
isExpired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! isExpired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Permission } from "./Permission";
|
||||||
|
|
||||||
|
export class Role
|
||||||
|
{
|
||||||
|
id:string;
|
||||||
|
inherits:string;
|
||||||
|
permissions:Permission[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Role } from "./Role";
|
||||||
|
|
||||||
|
export class RolesData
|
||||||
|
{
|
||||||
|
roles:Role[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { RequestHandler } from "../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { UserManagementServer } from "../UserManagementServer";
|
||||||
|
|
||||||
|
export abstract class RequestRequirement
|
||||||
|
{
|
||||||
|
_isGlobal:boolean = false;
|
||||||
|
_ums:UserManagementServer;
|
||||||
|
_handler:RequestHandler = null;
|
||||||
|
|
||||||
|
|
||||||
|
initialize( handler:RequestHandler ):Promise<void>
|
||||||
|
{
|
||||||
|
this._isGlobal = false;
|
||||||
|
this._handler = handler;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeGlobal( ums:UserManagementServer ):Promise<void>
|
||||||
|
{
|
||||||
|
this._isGlobal = true;
|
||||||
|
this._ums = ums;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import { RequestRequirement } from "../RequestRequirement";
|
||||||
|
import { UserManagementServer } from "../../UserManagementServer";
|
||||||
|
import { MapList } from "../../../../browser/tools/MapList";
|
||||||
|
import { DateHelper } from "../../../../browser/date/DateHelper";
|
||||||
|
import { Duration } from "../../../../browser/date/Duration";
|
||||||
|
import { DateMath } from "../../../../browser/date/DateMath";
|
||||||
|
|
||||||
|
|
||||||
|
export class NotTooManyRequests extends RequestRequirement
|
||||||
|
{
|
||||||
|
_trackedIPs = new Map<string,number[]>();
|
||||||
|
_maxRequests = 50;
|
||||||
|
_duration = Duration.fromMinutes( 2 );
|
||||||
|
|
||||||
|
_watchList = new Set<string>();
|
||||||
|
_blockList = new Map<string,number>()
|
||||||
|
_blockDuration = Duration.fromHours( 4 );
|
||||||
|
|
||||||
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean>
|
||||||
|
{
|
||||||
|
if ( this._blockList.has( request.ip ) )
|
||||||
|
{
|
||||||
|
let blockTime = this._blockList.get( request.ip );
|
||||||
|
|
||||||
|
if ( ! DateMath.isExpired( new Date( blockTime + this._blockDuration ) ) )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid = this.updateIP( request.ip );
|
||||||
|
|
||||||
|
return Promise.resolve( valid );
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIP( ip:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
MapList.add( this._trackedIPs, ip, DateHelper.nowMS() );
|
||||||
|
MapList.shiftToSize( this._trackedIPs, ip, this._maxRequests );
|
||||||
|
|
||||||
|
let lastRelevantDate = DateMath.addSeconds( DateHelper.now(), -this._duration );
|
||||||
|
|
||||||
|
let timeStamps = this._trackedIPs.get( ip );
|
||||||
|
let filteredStamps = timeStamps.filter( ts => DateMath.isAfter( lastRelevantDate, new Date( ts ) ) );
|
||||||
|
|
||||||
|
|
||||||
|
if ( filteredStamps.length < this._maxRequests )
|
||||||
|
{
|
||||||
|
this._trackedIPs.set( ip, filteredStamps );
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
this._trackedIPs.delete( ip );
|
||||||
|
|
||||||
|
if ( this._watchList.has( ip ) )
|
||||||
|
{
|
||||||
|
this._blockList.set( ip, DateHelper.nowMS() );
|
||||||
|
this._watchList.delete( ip );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._watchList.add( ip );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( false );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { RequestRequirement } from '../RequestRequirement';
|
||||||
|
|
||||||
|
export class UserHasPermission extends RequestRequirement
|
||||||
|
{
|
||||||
|
private _permissionID:string = "";
|
||||||
|
|
||||||
|
constructor( permissionID:string )
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._permissionID = permissionID;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean>
|
||||||
|
{
|
||||||
|
let user = await this._handler.getUser();
|
||||||
|
|
||||||
|
let userDB = this._handler.userDB;
|
||||||
|
|
||||||
|
return userDB.hasPermission( user, this._permissionID );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { RequestRequirement } from '../RequestRequirement';
|
||||||
|
|
||||||
|
export class UserIsLoggedIn extends RequestRequirement
|
||||||
|
{
|
||||||
|
|
||||||
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean>
|
||||||
|
{
|
||||||
|
let requestBody = JSON.parse( request.body as string );
|
||||||
|
let tokenData = requestBody as { token:string };
|
||||||
|
|
||||||
|
if ( ! tokenData )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
|
let session = this._handler._ums._sessions.get( tokenID );
|
||||||
|
|
||||||
|
if ( ! session )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = this._handler._ums.tokenDB._tokens.get( tokenID );
|
||||||
|
|
||||||
|
let isValid = await this._handler._ums.tokenDB.validate( token, request.ip );
|
||||||
|
|
||||||
|
if ( ! isValid )
|
||||||
|
{
|
||||||
|
return Promise.resolve( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve( true );
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue