Add Reminder App
This commit is contained in:
parent
5efc796edc
commit
2046fdf342
|
|
@ -1,9 +1,11 @@
|
||||||
import { TextTool } from "../text/TextTool";
|
import { TextTool } from "../text/TextTool";
|
||||||
|
import { DateHelper } from "./DateHelper";
|
||||||
|
|
||||||
export class DateFormatter
|
export class DateFormatter
|
||||||
{
|
{
|
||||||
static YMD_HMS( date:Date ):string
|
static YMD_HMS( date:Date = undefined ):string
|
||||||
{
|
{
|
||||||
|
date = date || DateHelper.now();
|
||||||
let ye = ( date.getFullYear() + "" ).substring( 2 );
|
let ye = ( date.getFullYear() + "" ).substring( 2 );
|
||||||
let mo = TextTool.prependZeros( ( date.getMonth() + 1 ) );
|
let mo = TextTool.prependZeros( ( date.getMonth() + 1 ) );
|
||||||
let da = TextTool.prependZeros( date.getDate() );
|
let da = TextTool.prependZeros( date.getDate() );
|
||||||
|
|
@ -15,6 +17,17 @@ export class DateFormatter
|
||||||
return `${ye}-${mo}-${da} ${h}-${m}-${s}`;
|
return `${ye}-${mo}-${da} ${h}-${m}-${s}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static HMS( date:Date = undefined ):string
|
||||||
|
{
|
||||||
|
date = date || DateHelper.now();
|
||||||
|
|
||||||
|
let h = TextTool.prependZeros( date.getHours() );
|
||||||
|
let m = TextTool.prependZeros( date.getMinutes() );
|
||||||
|
let s = TextTool.prependZeros( date.getSeconds() );
|
||||||
|
|
||||||
|
return `${h}:${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
static forUsers( date:Date ):string
|
static forUsers( date:Date ):string
|
||||||
{
|
{
|
||||||
let ye = ( date.getFullYear() + "" );
|
let ye = ( date.getFullYear() + "" );
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { DateExpressionLexer } from "../text/lexer/DateExpressionLexer";
|
||||||
|
import { LexerQuery } from "../text/lexer/LexerQuery";
|
||||||
|
|
||||||
export class DateHelper
|
export class DateHelper
|
||||||
{
|
{
|
||||||
static now()
|
static now()
|
||||||
|
|
@ -31,4 +34,62 @@ export class DateHelper
|
||||||
|
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseDateExpression( expression:string ):Date
|
||||||
|
{
|
||||||
|
let query = LexerQuery.from( expression, new DateExpressionLexer() );
|
||||||
|
|
||||||
|
let now = query.find( le => le.isMatcher( DateExpressionLexer.Now ) );
|
||||||
|
|
||||||
|
if ( now )
|
||||||
|
{
|
||||||
|
return DateHelper.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = query.find( le => le.isMatcher( DateExpressionLexer.Time ) );
|
||||||
|
let hourMinutesSeconds = ( time?.match || "9:00" ).split( ":" ).map( s => parseInt( s ) );
|
||||||
|
|
||||||
|
let hours = hourMinutesSeconds[ 0 ];
|
||||||
|
let minutes = hourMinutesSeconds[ 1 ];
|
||||||
|
let seconds = hourMinutesSeconds.length == 3 ? hourMinutesSeconds[ 2 ] : 0;
|
||||||
|
|
||||||
|
let year = DateHelper.now().getFullYear();
|
||||||
|
let month = DateHelper.now().getMonth();
|
||||||
|
let day = DateHelper.now().getDate();
|
||||||
|
|
||||||
|
let date = query.find( le => /date/i.test( le.type ) );
|
||||||
|
|
||||||
|
if ( date )
|
||||||
|
{
|
||||||
|
let seperator = /slash/i.test( date.type ) ? "/" : /dot/i.test( date.type ) ? "." : "-";
|
||||||
|
|
||||||
|
let values = date.match.replace( /(\/|\.|\-)$/, "" ).split( seperator ).map( s => parseInt( s ) );
|
||||||
|
|
||||||
|
if ( /reverse/i.test( date.type ) )
|
||||||
|
{
|
||||||
|
year = values[ 0 ];
|
||||||
|
month = values[ 1 ] - 1;
|
||||||
|
day = values[ 2 ];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
day = values[ 0 ];
|
||||||
|
month = values[ 1 ] - 1;
|
||||||
|
|
||||||
|
if ( values.length == 3 )
|
||||||
|
{
|
||||||
|
year = values[ 2 ];
|
||||||
|
|
||||||
|
if ( year < 100 )
|
||||||
|
{
|
||||||
|
year += 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateHelper.createYMD( year, month, day, hours, minutes, seconds );
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +74,8 @@ export class DateMath
|
||||||
|
|
||||||
static isAfterNow( d:Date )
|
static isAfterNow( d:Date )
|
||||||
{
|
{
|
||||||
return d.getTime() > DateHelper.now().getTime();
|
// return d.getTime() > DateHelper.now().getTime();
|
||||||
|
return DateMath.isAfter( d, DateHelper.now() );
|
||||||
}
|
}
|
||||||
|
|
||||||
static isInTheFuture( d:Date )
|
static isInTheFuture( d:Date )
|
||||||
|
|
@ -82,6 +83,11 @@ export class DateMath
|
||||||
return DateMath.isAfterNow( d );
|
return DateMath.isAfterNow( d );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isAheadInTheFuture( d:Date, duration:number )
|
||||||
|
{
|
||||||
|
return DateMath.isAfter( d, DateMath.addSeconds( DateHelper.now(), duration ) );
|
||||||
|
}
|
||||||
|
|
||||||
static getDifferenceMs( a:Date, b:Date )
|
static getDifferenceMs( a:Date, b:Date )
|
||||||
{
|
{
|
||||||
return a.getTime() - b.getTime();
|
return a.getTime() - b.getTime();
|
||||||
|
|
@ -115,16 +121,16 @@ export class DateMath
|
||||||
return DateMath.addMinutes( new Date(), minutes );
|
return DateMath.addMinutes( new Date(), minutes );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromNowAddSeconds( seconds:number )
|
||||||
|
{
|
||||||
|
return DateMath.addSeconds( new Date(), seconds );
|
||||||
|
}
|
||||||
|
|
||||||
static addSeconds( a:Date, duration:number )
|
static addSeconds( a:Date, duration:number )
|
||||||
{
|
{
|
||||||
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 );
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
export class Duration
|
export class Duration
|
||||||
{
|
{
|
||||||
|
static toMilliSeconds( duration:number )
|
||||||
|
{
|
||||||
|
return duration * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMilliSeconds( duration:number )
|
||||||
|
{
|
||||||
|
return duration / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
static toMinutes( duration:number )
|
static toMinutes( duration:number )
|
||||||
{
|
{
|
||||||
return duration / 60;
|
return duration / 60;
|
||||||
|
|
|
||||||
|
|
@ -112,3 +112,19 @@ export class OrExpression<T> extends ListInputExpression<T>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class LamdaExpression<T> extends BooleanExpression<T>
|
||||||
|
{
|
||||||
|
_lamda:(t:T)=>boolean;
|
||||||
|
|
||||||
|
constructor( lamda:(t:T)=>boolean )
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._lamda = lamda;
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate( t:T )
|
||||||
|
{
|
||||||
|
return this._lamda( t );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { JSRandomEngine } from "./JSRandomEngine";
|
||||||
|
|
||||||
|
export class CryptoTool
|
||||||
|
{
|
||||||
|
static randomUUID():string
|
||||||
|
{
|
||||||
|
if ( window.location.protocol !== "https" )
|
||||||
|
{
|
||||||
|
let random = JSRandomEngine.$;
|
||||||
|
|
||||||
|
let structure = [8,4,4,4,12];
|
||||||
|
|
||||||
|
let id = "";
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
for ( let j = 0; j < structure.length; j++ )
|
||||||
|
{
|
||||||
|
if ( first ){ first = false; }
|
||||||
|
else{ id +="-"; }
|
||||||
|
|
||||||
|
let num = structure[ j ];
|
||||||
|
|
||||||
|
for ( let i = 0; i < num; i++ )
|
||||||
|
{
|
||||||
|
id += random.fromString( "0123456789abcdef" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Lexer } from "./Lexer";
|
||||||
|
import { LexerMatcher } from "./LexerMatcher";
|
||||||
|
import { LexerMatcherLibrary } from "./LexerMatcherLibrary";
|
||||||
|
|
||||||
|
export class DateExpressionLexer extends Lexer
|
||||||
|
{
|
||||||
|
static readonly Now = new LexerMatcher( "Now", /now|jetzt/i );
|
||||||
|
static readonly Time = new LexerMatcher( "Time", /\d?\d\:\d\d(\:\d\d)?\s*(am|h|uhr)?/i );
|
||||||
|
|
||||||
|
static readonly LongDateMinus = new LexerMatcher( "LongDateMinus", /\d?\d\-\d?\d\-\d\d\d\d/ );
|
||||||
|
static readonly LongDateDot = new LexerMatcher( "LongDateDot", /\d?\d\.\d?\d\.\d\d\d\d/ );
|
||||||
|
static readonly LongDateSlash = new LexerMatcher( "LongDateSlash", /\d?\d\/\d?\d\/\d\d\d\d/ );
|
||||||
|
|
||||||
|
static readonly ReverseLongDateMinus = new LexerMatcher( "ReverseLongDateMinus", /\d\d\d\d\-\d?\d\-\d?\d/ );
|
||||||
|
static readonly ReverseLongDateDot = new LexerMatcher( "ReverseLongDateDot", /\d\d\d\d\.\d?\d\.\d?\d/ );
|
||||||
|
static readonly ReverseLongDateSlash = new LexerMatcher( "ReverseLongDateSlash", /\d\d\d\d\/\d?\d\/\d?\d/ );
|
||||||
|
|
||||||
|
static readonly DateMinus = new LexerMatcher( "DateMinus", /\d?\d\-\d?\d\-\d\d/ );
|
||||||
|
static readonly DateDot = new LexerMatcher( "DateDot", /\d?\d\.\d?\d\.\d\d/ );
|
||||||
|
static readonly DateSlash = new LexerMatcher( "DateSlash", /\d?\d\/\d?\d\/\d\d/ );
|
||||||
|
|
||||||
|
static readonly ShortDateMinus = new LexerMatcher( "ShortDateMinus", /\d?\d\-\d?\d(\-)?/ );
|
||||||
|
static readonly ShortDateDot = new LexerMatcher( "ShortDateDot", /\d?\d\.\d?\d(\.)?/ );
|
||||||
|
static readonly ShortDateSlash = new LexerMatcher( "ShortDateSlash", /\d?\d\/\d?\d(\/)?/ );
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.addAllMatchers(
|
||||||
|
|
||||||
|
DateExpressionLexer.Now,
|
||||||
|
DateExpressionLexer.Time,
|
||||||
|
|
||||||
|
DateExpressionLexer.LongDateMinus,
|
||||||
|
DateExpressionLexer.LongDateDot,
|
||||||
|
DateExpressionLexer.LongDateSlash,
|
||||||
|
|
||||||
|
DateExpressionLexer.ReverseLongDateMinus,
|
||||||
|
DateExpressionLexer.ReverseLongDateDot,
|
||||||
|
DateExpressionLexer.ReverseLongDateSlash,
|
||||||
|
|
||||||
|
DateExpressionLexer.DateMinus,
|
||||||
|
DateExpressionLexer.DateDot,
|
||||||
|
DateExpressionLexer.DateSlash,
|
||||||
|
|
||||||
|
DateExpressionLexer.ShortDateMinus,
|
||||||
|
DateExpressionLexer.ShortDateDot,
|
||||||
|
DateExpressionLexer.ShortDateSlash,
|
||||||
|
|
||||||
|
LexerMatcherLibrary.WHITESPACE_MATCHER,
|
||||||
|
LexerMatcherLibrary.BREAK_MATCHER,
|
||||||
|
LexerMatcherLibrary.ANY_SYMBOL_MATCHER
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { LexerMatcher } from "./LexerMatcher";
|
import { LexerMatcher } from "./LexerMatcher";
|
||||||
import { LexerEvent } from "./LexerEvent";
|
import { LexerEvent } from "./LexerEvent";
|
||||||
|
import { LexerQuery } from "./LexerQuery";
|
||||||
|
|
||||||
export class Lexer
|
export class Lexer
|
||||||
{
|
{
|
||||||
|
|
@ -128,6 +129,14 @@ export class Lexer
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLexerQuery( source:string, offset:number = 0, mode:string = Lexer.defaultMode )
|
||||||
|
{
|
||||||
|
let query = new LexerQuery();
|
||||||
|
query.source = source;
|
||||||
|
query.tokens = this.lexTokens( source, offset, mode );
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
lexToList( source:string, offset:number = 0, mode:string = Lexer.defaultMode )
|
lexToList( source:string, offset:number = 0, mode:string = Lexer.defaultMode )
|
||||||
{
|
{
|
||||||
var list:LexerEvent[] = [];
|
var list:LexerEvent[] = [];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { LexerMatcher } from "./LexerMatcher";
|
||||||
|
|
||||||
export class LexerEvent
|
export class LexerEvent
|
||||||
{
|
{
|
||||||
static readonly LENGTH_ERROR_ID = -1;
|
static readonly LENGTH_ERROR_ID = -1;
|
||||||
|
|
@ -41,7 +43,12 @@ export class LexerEvent
|
||||||
|
|
||||||
isType( type:string )
|
isType( type:string )
|
||||||
{
|
{
|
||||||
return this._type == type;
|
return this._type === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMatcher( lexerMatcher:LexerMatcher )
|
||||||
|
{
|
||||||
|
return this._type === lexerMatcher.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isError(){ return this._length === LexerEvent.LENGTH_ERROR_ID; }
|
get isError(){ return this._length === LexerEvent.LENGTH_ERROR_ID; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { BooleanExpression } from "../../expressions/BooleanExpression";
|
import { BooleanExpression, LamdaExpression } from "../../expressions/BooleanExpression";
|
||||||
|
import { ClassConstructor, isClassOf } from "../../tools/TypeUtilities";
|
||||||
|
import { Lexer } from "./Lexer";
|
||||||
import { LexerEvent } from "./LexerEvent";
|
import { LexerEvent } from "./LexerEvent";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -9,6 +11,11 @@ export class LexerQuery
|
||||||
|
|
||||||
_index:Map<LexerEvent,number> = new Map<LexerEvent,number>();
|
_index:Map<LexerEvent,number> = new Map<LexerEvent,number>();
|
||||||
|
|
||||||
|
static from( source:string, lexer:Lexer )
|
||||||
|
{
|
||||||
|
return lexer.createLexerQuery( source );
|
||||||
|
}
|
||||||
|
|
||||||
createTokenIndex()
|
createTokenIndex()
|
||||||
{
|
{
|
||||||
for ( let i = 0; i < this.tokens.length; i++ )
|
for ( let i = 0; i < this.tokens.length; i++ )
|
||||||
|
|
@ -258,4 +265,20 @@ export class LexerQuery
|
||||||
|
|
||||||
return nextIndex === -1 ? null : this.tokens[ nextIndex ];
|
return nextIndex === -1 ? null : this.tokens[ nextIndex ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find( matcherOrPredicate:BooleanExpression<LexerEvent>|( (l:LexerEvent)=>boolean ) )
|
||||||
|
{
|
||||||
|
let matcher:BooleanExpression<LexerEvent> = null;
|
||||||
|
|
||||||
|
if ( isClassOf( matcher, BooleanExpression ) )
|
||||||
|
{
|
||||||
|
matcher = matcherOrPredicate as BooleanExpression<LexerEvent>;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matcher = new LamdaExpression<LexerEvent>( matcherOrPredicate as (l:LexerEvent)=>boolean );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.searchItem( 0, true, matcher );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class ErrorInfo
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,9 @@ export abstract class RequestHandler
|
||||||
{
|
{
|
||||||
|
|
||||||
_ums:UserManagementServer;
|
_ums:UserManagementServer;
|
||||||
|
|
||||||
|
get ums(){ return this._ums; };
|
||||||
|
|
||||||
_type:RequestType;
|
_type:RequestType;
|
||||||
_url:string;
|
_url:string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ export class UserDB
|
||||||
return this.users.findIndex( u => u.email === email ) != -1;
|
return this.users.findIndex( u => u.email === email ) != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hasUserWithID( id:string ):Promise<boolean>
|
||||||
|
{
|
||||||
|
return this.users.findIndex( u => u.id === id ) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
async signUp( email:string, password:string, userName:string = "User" ):Promise<UserData>
|
async signUp( email:string, password:string, userName:string = "User" ):Promise<UserData>
|
||||||
{
|
{
|
||||||
let userData = this._pendingUsers.find( u => u.email === email );
|
let userData = this._pendingUsers.find( u => u.email === email );
|
||||||
|
|
@ -43,6 +48,8 @@ export class UserDB
|
||||||
userData.email = email;
|
userData.email = email;
|
||||||
userData.hashedPassword = await CryptIO.hash( password );
|
userData.hashedPassword = await CryptIO.hash( password );
|
||||||
userData.name = userName;
|
userData.name = userName;
|
||||||
|
userData.role = Role.User;
|
||||||
|
userData.permissions = [];
|
||||||
|
|
||||||
this._pendingUsers.push( userData );
|
this._pendingUsers.push( userData );
|
||||||
|
|
||||||
|
|
@ -208,8 +215,10 @@ export class UserDB
|
||||||
return Promise.resolve( false );
|
return Promise.resolve( false );
|
||||||
}
|
}
|
||||||
|
|
||||||
async _hasPermission( permissionID:string, permissions:Permission[] ):Promise<boolean>
|
protected async _hasPermission( permissionID:string, permissions:Permission[] ):Promise<boolean>
|
||||||
{
|
{
|
||||||
|
permissions = permissions || [];
|
||||||
|
|
||||||
for ( let p of permissions )
|
for ( let p of permissions )
|
||||||
{
|
{
|
||||||
if ( Permission.isMatching( permissionID, p ) )
|
if ( Permission.isMatching( permissionID, p ) )
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import Fastify, { FastifyHttpsOptions, FastifyInstance, FastifyListenOptions } from "fastify";
|
import Fastify, { FastifyHttpsOptions, FastifyInstance, FastifyListenOptions, FastifyRequest } from "fastify";
|
||||||
import fastifyMultipart from "@fastify/multipart";
|
import fastifyMultipart from "@fastify/multipart";
|
||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
|
|
||||||
|
import { EventSlot } from "../../browser/events/EventSlot";
|
||||||
|
import { JSRandomEngine } from "../../browser/random/JSRandomEngine";
|
||||||
|
|
||||||
import { UserDB } from "./UserDB";
|
import { UserDB } from "./UserDB";
|
||||||
import { RequestHandler } from "./RequestHandler";
|
import { RequestHandler } from "./RequestHandler";
|
||||||
import { EmailService as EmailService } from "./email/EmailService";
|
import { EmailService as EmailService } from "./email/EmailService";
|
||||||
|
|
@ -16,6 +19,15 @@ import { LocationService } from "./location/LocationService";
|
||||||
import { RequestRequirement } from "./requirements/RequestRequirement";
|
import { RequestRequirement } from "./requirements/RequestRequirement";
|
||||||
import { NotTooManyRequests } from "./requirements/security/NotTooManyRequests";
|
import { NotTooManyRequests } from "./requirements/security/NotTooManyRequests";
|
||||||
import { FilesSync } from "../files/FilesSync";
|
import { FilesSync } from "../files/FilesSync";
|
||||||
|
import { UserData } from "./UserData";
|
||||||
|
import { Scheduler } from "./scheduler/Scheduler";
|
||||||
|
import { DateHelper } from "../../browser/date/DateHelper";
|
||||||
|
import { DateFormatter } from "../../browser/date/DateFormatter";
|
||||||
|
import { Duration } from "../../browser/date/Duration";
|
||||||
|
import { Task } from "./scheduler/Task";
|
||||||
|
import { UserApp } from "./apps/UserApp";
|
||||||
|
import { UserAppFactory } from "./apps/UserAppFactory";
|
||||||
|
import { iTaskScheduler } from "./scheduler/iTaskScheduler";
|
||||||
|
|
||||||
export class UserManagementServer
|
export class UserManagementServer
|
||||||
{
|
{
|
||||||
|
|
@ -43,6 +55,14 @@ export class UserManagementServer
|
||||||
_globalRequirements:RequestRequirement[] = [];
|
_globalRequirements:RequestRequirement[] = [];
|
||||||
get globalRequirements(){ return this._globalRequirements; }
|
get globalRequirements(){ return this._globalRequirements; }
|
||||||
|
|
||||||
|
_apps:UserApp<any>[] = [];
|
||||||
|
get apps(){ return this._apps; }
|
||||||
|
|
||||||
|
_scheduler:Scheduler;
|
||||||
|
get scheduler(){ return this._scheduler; }
|
||||||
|
|
||||||
|
readonly onFileChanged = new EventSlot<string>();
|
||||||
|
|
||||||
|
|
||||||
async initialize( settings:UserManagementServerSettings, mailService:EmailService, handlers:RequestHandler[], globalRequirements:RequestRequirement[] = null ):Promise<void>
|
async initialize( settings:UserManagementServerSettings, mailService:EmailService, handlers:RequestHandler[], globalRequirements:RequestRequirement[] = null ):Promise<void>
|
||||||
{
|
{
|
||||||
|
|
@ -54,9 +74,12 @@ export class UserManagementServer
|
||||||
await this._addGlobalRequirements( globalRequirements || UserManagementServer.DefaultGlobalRequirements() );
|
await this._addGlobalRequirements( globalRequirements || UserManagementServer.DefaultGlobalRequirements() );
|
||||||
await this._addServices( mailService );
|
await this._addServices( mailService );
|
||||||
await this._addHandlers( handlers );
|
await this._addHandlers( handlers );
|
||||||
|
await this._startApps();
|
||||||
|
|
||||||
await this._startServer();
|
await this._startServer();
|
||||||
|
|
||||||
|
this._update();
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +106,16 @@ export class UserManagementServer
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUser( request:FastifyRequest ):Promise<UserData>
|
||||||
|
{
|
||||||
|
let requestBody = request.body;
|
||||||
|
let tokenData = requestBody as { token:string };
|
||||||
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
|
let session = this._sessions.get( tokenID );
|
||||||
|
return this.userDB.byID( session.userID );
|
||||||
|
}
|
||||||
|
|
||||||
async sendEmail( to:string, title:string, message:string ):Promise<void>
|
async sendEmail( to:string, title:string, message:string ):Promise<void>
|
||||||
{
|
{
|
||||||
return this.email.send( this._settings.emailFrom, to, title, message );
|
return this.email.send( this._settings.emailFrom, to, title, message );
|
||||||
|
|
@ -115,27 +148,52 @@ export class UserManagementServer
|
||||||
|
|
||||||
this._app.register( fastifyMultipart );
|
this._app.register( fastifyMultipart );
|
||||||
|
|
||||||
for ( let corsURL of this._settings.corsURLs )
|
if ( this._settings.isDebugMode )
|
||||||
{
|
{
|
||||||
RJLog.log( "Adding cors:", corsURL );
|
RJLog.log( "Setting any cors:" );
|
||||||
|
|
||||||
await this._app.register( cors,
|
await this._app.register( cors,
|
||||||
{
|
{
|
||||||
origin: corsURL,
|
origin: "*",
|
||||||
credentials: true,
|
});
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
}
|
||||||
}
|
else
|
||||||
);
|
{
|
||||||
|
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();
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDuration = 60;
|
||||||
|
|
||||||
|
protected _update()
|
||||||
|
{
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
let waitTimeMS = Duration.toMilliSeconds( this._updateDuration );
|
||||||
|
|
||||||
|
setTimeout( () =>{ self._update() }, waitTimeMS );
|
||||||
}
|
}
|
||||||
|
|
||||||
async _addServices( mailService:EmailService ):Promise<void>
|
async _addServices( mailService:EmailService ):Promise<void>
|
||||||
{
|
{
|
||||||
this._userDB = await UserDB.load( this, this._settings.userDBPath );
|
this._userDB = await UserDB.load( this, this._settings.userDBPath );
|
||||||
|
|
||||||
RJLog.log( "Loading roles:", this._settings.rolesPath );
|
this._scheduler = new Scheduler();
|
||||||
|
|
||||||
|
// RJLog.log( "Loading roles:", this._settings.rolesPath );
|
||||||
let rolesData = await Files.loadJSON<RolesData>( this._settings.rolesPath );
|
let rolesData = await Files.loadJSON<RolesData>( this._settings.rolesPath );
|
||||||
this._roles = new Map<string,Role>();
|
this._roles = new Map<string,Role>();
|
||||||
rolesData.roles.forEach( r => this._roles.set( r.id, r ) );
|
rolesData.roles.forEach( r => this._roles.set( r.id, r ) );
|
||||||
|
|
@ -177,10 +235,10 @@ export class UserManagementServer
|
||||||
port: this._settings.port
|
port: this._settings.port
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! this._settings.isDebugMode )
|
// if ( ! this._settings.isDebugMode )
|
||||||
{
|
// {
|
||||||
listenOptions.host = "0.0.0.0";
|
listenOptions.host = "0.0.0.0";
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.app.listen(
|
this.app.listen(
|
||||||
|
|
||||||
|
|
@ -200,4 +258,25 @@ export class UserManagementServer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _startApps()
|
||||||
|
{
|
||||||
|
for ( let appID of this._settings.userApps )
|
||||||
|
{
|
||||||
|
let app = UserAppFactory.create( appID, this );
|
||||||
|
RJLog.log( "Starting app:", app.id );
|
||||||
|
this._apps.push( app );
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( let app of this._apps )
|
||||||
|
{
|
||||||
|
await app.initialize();
|
||||||
|
|
||||||
|
if ( app.schedulesTasks )
|
||||||
|
{
|
||||||
|
this.scheduler._taskSchedulers.push( app as any as iTaskScheduler );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._scheduler.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,10 @@ export class UserManagementServerSettings
|
||||||
userDBPath:string;
|
userDBPath:string;
|
||||||
rolesPath:string;
|
rolesPath:string;
|
||||||
|
|
||||||
|
// Apps
|
||||||
|
appsPath:string;
|
||||||
|
userApps:string[];
|
||||||
|
|
||||||
// Geo-Lite Lib
|
// Geo-Lite Lib
|
||||||
geoLocationPath:string;
|
geoLocationPath:string;
|
||||||
geoAccountID:string;
|
geoAccountID:string;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { EventSlot } from "../../../browser/events/EventSlot";
|
||||||
|
import { Files } from "../../files/Files";
|
||||||
|
import { PathFilter } from "../../files/PathFilter";
|
||||||
|
import { UserData } from "../UserData";
|
||||||
|
import { UserManagementServer } from "../UserManagementServer";
|
||||||
|
|
||||||
|
export class UserApp<AppData>
|
||||||
|
{
|
||||||
|
protected _id:string;
|
||||||
|
get id(){ return this._id;}
|
||||||
|
|
||||||
|
protected _ums:UserManagementServer;
|
||||||
|
get ums(){ return this._ums }
|
||||||
|
|
||||||
|
protected _path:string;
|
||||||
|
get path(){ return this._path; }
|
||||||
|
|
||||||
|
readonly onFileChange = new EventSlot<string>();
|
||||||
|
|
||||||
|
constructor( id:string, ums:UserManagementServer )
|
||||||
|
{
|
||||||
|
this._ums = ums;
|
||||||
|
this._id = id;
|
||||||
|
|
||||||
|
this._path = Files.joinPaths( [ this._ums._settings.appsPath, this._id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize():Promise<void>
|
||||||
|
{
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
get schedulesTasks():boolean
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserPath( userID:string ){ return Files.joinPaths( [ this.path, userID + ".json" ] ); }
|
||||||
|
|
||||||
|
async iterateUserData( action:(ud:UserData,ad:AppData)=>Promise<void>):Promise<void>
|
||||||
|
{
|
||||||
|
await Files.forDirectChildrenIn( this.path, PathFilter.JSON,
|
||||||
|
async ( file ) =>
|
||||||
|
{
|
||||||
|
let id = file.fileNameWithoutExtension;
|
||||||
|
|
||||||
|
if ( ! this.ums.userDB.hasUserWithID( id ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData = await this.ums.userDB.byID( id );
|
||||||
|
|
||||||
|
let appData = await Files.loadJSON<AppData>( file.absolutePath );
|
||||||
|
|
||||||
|
return action( userData, appData );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { UserManagementServer } from "../UserManagementServer";
|
||||||
|
import { ReminderApp } from "./reminder/ReminderApp";
|
||||||
|
import { UserApp } from "./UserApp";
|
||||||
|
|
||||||
|
export class UserAppFactory
|
||||||
|
{
|
||||||
|
static create( id:string, ums:UserManagementServer ):UserApp<any>
|
||||||
|
{
|
||||||
|
let constructors = [ ReminderApp ];
|
||||||
|
|
||||||
|
for ( let c of constructors )
|
||||||
|
{
|
||||||
|
if ( c.id === id )
|
||||||
|
{
|
||||||
|
return new c( ums );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { DateHelper } from "../../../../browser/date/DateHelper";
|
||||||
|
import { DateMath } from "../../../../browser/date/DateMath";
|
||||||
|
import { ISOTimeStamp } from "../../../../browser/date/ISOTimeStamp";
|
||||||
|
import { PathReference } from "../../../files/PathReference";
|
||||||
|
import { iTaskScheduler } from "../../scheduler/iTaskScheduler";
|
||||||
|
import { Task } from "../../scheduler/Task";
|
||||||
|
import { UserData } from "../../UserData";
|
||||||
|
import { UserManagementServer } from "../../UserManagementServer";
|
||||||
|
import { UserApp } from "../UserApp";
|
||||||
|
import { MailEntry } from "./data/MailEntry";
|
||||||
|
import { ReminderData } from "./data/ReminderData";
|
||||||
|
|
||||||
|
export class ReminderApp extends UserApp<ReminderData> implements iTaskScheduler
|
||||||
|
{
|
||||||
|
static readonly id = "reminder";
|
||||||
|
|
||||||
|
constructor( ums:UserManagementServer )
|
||||||
|
{
|
||||||
|
super( ReminderApp.id, ums );
|
||||||
|
this.onFileChange.addListener(
|
||||||
|
async ( filePath )=>
|
||||||
|
{
|
||||||
|
let pathReference = new PathReference( filePath );
|
||||||
|
let id = pathReference.fileNameWithoutExtension;
|
||||||
|
let user = await this.ums.userDB.byID( id );
|
||||||
|
|
||||||
|
if ( ! user )
|
||||||
|
{
|
||||||
|
console.log( "file changed:", filePath, "but no user associated" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await pathReference.loadJSON<ReminderData>();
|
||||||
|
|
||||||
|
let tasks:Task[] =[];
|
||||||
|
this.grabTasks( user, data, this.ums.scheduler.maxDate, tasks );
|
||||||
|
this.ums.scheduler.scheduleTasks( tasks );
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTasksToSchedule( maxDate:Date ):Promise<Task[]>
|
||||||
|
{
|
||||||
|
let tasks = [];
|
||||||
|
|
||||||
|
await this.iterateUserData(
|
||||||
|
( userData:UserData, reminderData:ReminderData )=>
|
||||||
|
{
|
||||||
|
this.grabTasks( userData, reminderData, maxDate, tasks );
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.resolve( tasks );
|
||||||
|
}
|
||||||
|
|
||||||
|
grabTasks( userData:UserData, rd:ReminderData, maxDate:Date, tasks:Task[] )
|
||||||
|
{
|
||||||
|
rd.mailEntries.forEach(
|
||||||
|
( me )=>
|
||||||
|
{
|
||||||
|
let date = DateHelper.parseDateExpression( me.date );
|
||||||
|
|
||||||
|
if ( DateMath.isBefore( date, maxDate ) )
|
||||||
|
{
|
||||||
|
let task = Task.createAt( date,
|
||||||
|
()=>
|
||||||
|
{
|
||||||
|
this.ums.sendEmail( userData.email, me.subject, me.message );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push( task )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
import { DateHelper } from "../../../../../browser/date/DateHelper";
|
||||||
|
import { ISOTimeStamp } from "../../../../../browser/date/ISOTimeStamp";
|
||||||
|
import { DateExpressionLexer } from "../../../../../browser/text/lexer/DateExpressionLexer";
|
||||||
|
import { LexerQuery } from "../../../../../browser/text/lexer/LexerQuery";
|
||||||
|
import { RegExpUtility } from "../../../../../browser/text/RegExpUtitlity";
|
||||||
|
import { ReminderEntry, ReminderEntryType } from "./ReminderEntry";
|
||||||
|
|
||||||
|
export class MailEntry extends ReminderEntry
|
||||||
|
{
|
||||||
|
date:string;
|
||||||
|
subject:string;
|
||||||
|
message:string;
|
||||||
|
|
||||||
|
static create()
|
||||||
|
{
|
||||||
|
let me = new MailEntry();
|
||||||
|
ReminderEntry.initialize( me );
|
||||||
|
|
||||||
|
return me;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { MailEntry } from "./MailEntry";
|
||||||
|
|
||||||
|
export class ReminderData
|
||||||
|
{
|
||||||
|
mailEntries:MailEntry[];
|
||||||
|
|
||||||
|
static create()
|
||||||
|
{
|
||||||
|
let rd = new ReminderData();
|
||||||
|
rd.mailEntries = [];
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ISOTimeStamp } from "../../../../../browser/date/ISOTimeStamp";
|
||||||
|
import { CryptoTool } from "../../../../../browser/random/CryptoTool";
|
||||||
|
|
||||||
|
export enum ReminderEntryType
|
||||||
|
{
|
||||||
|
Mail, Clock, Calender
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReminderEntry
|
||||||
|
{
|
||||||
|
type:ReminderEntryType;
|
||||||
|
id:string;
|
||||||
|
creationTime:ISOTimeStamp;
|
||||||
|
|
||||||
|
static initialize( r:ReminderEntry )
|
||||||
|
{
|
||||||
|
r.id = CryptoTool.randomUUID();
|
||||||
|
r.type = ReminderEntryType.Mail;
|
||||||
|
r.creationTime = ISOTimeStamp.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,9 @@ import { LoginHandler } from "./_/login";
|
||||||
import { LogoutHandler } from "./_/logout";
|
import { LogoutHandler } from "./_/logout";
|
||||||
import { RequestPasswordChangeHandler } from "./_/request-password-change";
|
import { RequestPasswordChangeHandler } from "./_/request-password-change";
|
||||||
import { SignUpHandler } from "./_/signup";
|
import { SignUpHandler } from "./_/signup";
|
||||||
|
import { AppsCanUseHandler } from "./apps/can-use";
|
||||||
|
import { AppsLoadHandler } from "./apps/load";
|
||||||
|
import { AppsSaveHandler } from "./apps/save";
|
||||||
|
|
||||||
export class HandlerGroups
|
export class HandlerGroups
|
||||||
{
|
{
|
||||||
|
|
@ -21,7 +24,12 @@ export class HandlerGroups
|
||||||
new InfoHandler(),
|
new InfoHandler(),
|
||||||
|
|
||||||
new RequestPasswordChangeHandler(),
|
new RequestPasswordChangeHandler(),
|
||||||
new ChangePasswordHandler()
|
new ChangePasswordHandler(),
|
||||||
|
|
||||||
|
new AppsCanUseHandler(),
|
||||||
|
new AppsSaveHandler(),
|
||||||
|
new AppsLoadHandler(),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return defaultHandlers;
|
return defaultHandlers;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export class ConfirmSignUpHandler extends RequestHandler
|
||||||
|
|
||||||
if ( ! result )
|
if ( ! result )
|
||||||
{
|
{
|
||||||
return this.sendError( "User signup confimration failed" );
|
return this.sendError( "User signup confirmation failed" );
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sendInfo( "User creation confirmed" );
|
return this.sendInfo( "User creation confirmed" );
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { Message } from '../../../../browser/messages/Message';
|
||||||
|
import { RequestRequirement } from '../../requirements/RequestRequirement';
|
||||||
|
import { Permission } from '../../permissions/Permission';
|
||||||
|
|
||||||
|
export class UserCanUseApp extends RequestRequirement
|
||||||
|
{
|
||||||
|
|
||||||
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
|
||||||
|
{
|
||||||
|
let requestBody = request.body;
|
||||||
|
let appData = requestBody as { token:string, appID:string };
|
||||||
|
|
||||||
|
if ( ! appData )
|
||||||
|
{
|
||||||
|
return this.sendError( "No token data" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.ums._settings.userApps.indexOf( appData.appID ) == -1 )
|
||||||
|
{
|
||||||
|
return this.sendError( "Invalid app id: " + appData.appID + ". Defined apps:" + this.ums._settings.userApps.join( ", " ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.ums.getUser( request );
|
||||||
|
|
||||||
|
let appPermissionID = "apps." + appData.appID;
|
||||||
|
|
||||||
|
let hasPermission = await this.ums.userDB.hasPermission( user, appPermissionID );
|
||||||
|
|
||||||
|
if ( ! hasPermission )
|
||||||
|
{
|
||||||
|
return this.sendError( "Permission for app not found" );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.giveOK();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { RequestHandler, RequestType } from "../../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn";
|
||||||
|
import { UserCanUseApp } from "./UserCanUseApp";
|
||||||
|
|
||||||
|
|
||||||
|
export class AppsCanUseHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/apps/can-use";
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super( RequestType.POST, AppsCanUseHandler.url, [ new UserIsLoggedIn(), new UserCanUseApp() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
return this.sendInfo( "Can use" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { RequestHandler, RequestType } from "../../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn";
|
||||||
|
import { UserCanUseApp } from "./UserCanUseApp";
|
||||||
|
import { FilesSync } from "../../../files/FilesSync";
|
||||||
|
import { Files } from "../../../files/Files";
|
||||||
|
|
||||||
|
|
||||||
|
export class AppsLoadHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/apps/load";
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super( RequestType.POST, AppsLoadHandler.url, [ new UserIsLoggedIn(), new UserCanUseApp() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = request.body;
|
||||||
|
let appData = requestBody as { appData: any, appID:string };
|
||||||
|
|
||||||
|
let user = await this.getUser();
|
||||||
|
let filePath = Files.joinPaths( [ this._ums._settings.appsPath, appData.appID, user.id + ".json" ] );
|
||||||
|
|
||||||
|
let exists = await Files.exists( filePath );
|
||||||
|
|
||||||
|
if ( ! exists )
|
||||||
|
{
|
||||||
|
return this.sendInfo( "No data saved" );
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await Files.loadJSON<any>( filePath );
|
||||||
|
|
||||||
|
return this.sendDataInfo( "Data loaded", data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { RequestHandler, RequestType } from "../../RequestHandler";
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn";
|
||||||
|
import { UserCanUseApp } from "./UserCanUseApp";
|
||||||
|
import { FilesSync } from "../../../files/FilesSync";
|
||||||
|
import { Files } from "../../../files/Files";
|
||||||
|
|
||||||
|
|
||||||
|
export class AppsSaveHandler extends RequestHandler
|
||||||
|
{
|
||||||
|
static url = "/apps/save";
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super( RequestType.POST, AppsSaveHandler.url, [ new UserIsLoggedIn(), new UserCanUseApp() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle( request:FastifyRequest, reply:FastifyReply )
|
||||||
|
{
|
||||||
|
let requestBody = request.body;
|
||||||
|
let appData = requestBody as { appData: any, appID:string };
|
||||||
|
|
||||||
|
let user = await this.getUser();
|
||||||
|
let filePath = Files.joinPaths( [ this._ums._settings.appsPath, appData.appID, user.id + ".json" ] );
|
||||||
|
|
||||||
|
await Files.ensureParentDirectoryExists( filePath );
|
||||||
|
await Files.saveJSON( filePath, appData.appData, true );
|
||||||
|
|
||||||
|
let app = this._ums.apps.find( a => a.id === appData.appID );
|
||||||
|
this._ums.onFileChanged.dispatch( filePath );
|
||||||
|
app.onFileChange.dispatch( filePath );
|
||||||
|
|
||||||
|
return this.sendInfo( "Data saved" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@ import { Permission } from "./Permission";
|
||||||
|
|
||||||
export class Role
|
export class Role
|
||||||
{
|
{
|
||||||
|
static readonly User = "user";
|
||||||
|
static readonly Admin = "admin";
|
||||||
|
|
||||||
id:string;
|
id:string;
|
||||||
inherits:string;
|
inherits:string;
|
||||||
permissions:Permission[];
|
permissions:Permission[];
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ import { RequestHandler } from "../RequestHandler";
|
||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import { UserManagementServer } from "../UserManagementServer";
|
import { UserManagementServer } from "../UserManagementServer";
|
||||||
import { Message } from "../../../browser/messages/Message";
|
import { Message } from "../../../browser/messages/Message";
|
||||||
|
import { ErrorInfo } from "../ErrorInfo";
|
||||||
|
import { MessageTypes } from "../../../browser/messages/MessageType";
|
||||||
|
|
||||||
export abstract class RequestRequirement
|
export abstract class RequestRequirement
|
||||||
{
|
{
|
||||||
_isGlobal:boolean = false;
|
private _isGlobal:boolean = false;
|
||||||
_ums:UserManagementServer;
|
private _ums:UserManagementServer;
|
||||||
_handler:RequestHandler = null;
|
private _handler:RequestHandler = null;
|
||||||
|
get handler(){ return this._handler; }
|
||||||
|
get ums() { return this._isGlobal ? this._ums : this._handler._ums };
|
||||||
|
|
||||||
|
|
||||||
initialize( handler:RequestHandler ):Promise<void>
|
initialize( handler:RequestHandler ):Promise<void>
|
||||||
|
|
@ -25,6 +29,17 @@ export abstract class RequestRequirement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sendError( message:string, errorInfo:ErrorInfo = undefined ):Promise<Message[]>
|
||||||
|
{
|
||||||
|
let messages = [ Message.Error( message ) ];
|
||||||
|
return Promise.resolve( messages );
|
||||||
|
}
|
||||||
|
|
||||||
|
giveOK():Promise<Message[]>
|
||||||
|
{
|
||||||
|
return Promise.resolve( [] );
|
||||||
|
}
|
||||||
|
|
||||||
abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
|
abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ export class NotTooManyRequests extends RequestRequirement
|
||||||
|
|
||||||
if ( ! DateMath.isExpired( new Date( blockTime + this._blockDuration ) ) )
|
if ( ! DateMath.isExpired( new Date( blockTime + this._blockDuration ) ) )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "User blocked" ) ] );
|
return this.sendError( "User blocked" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,10 +34,10 @@ export class NotTooManyRequests extends RequestRequirement
|
||||||
|
|
||||||
if ( ! valid )
|
if ( ! valid )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "Too many requests" ) ] );
|
return this.sendError( "Too many requests" );
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve( [] );
|
return this.giveOK();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateIP( ip:string ):Promise<boolean>
|
async updateIP( ip:string ):Promise<boolean>
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,17 @@ export class UserHasPermission extends RequestRequirement
|
||||||
|
|
||||||
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
|
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
|
||||||
{
|
{
|
||||||
let user = await this._handler.getUser();
|
let user = await this.ums.getUser( request );
|
||||||
|
|
||||||
let userDB = this._handler.userDB;
|
let userDB = this.ums.userDB;
|
||||||
|
|
||||||
let hasPermission = await userDB.hasPermission( user, this._permissionID );
|
let hasPermission = await userDB.hasPermission( user, this._permissionID );
|
||||||
|
|
||||||
if ( ! hasPermission )
|
if ( ! hasPermission )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "User has no permission for " + this._permissionID ) ] );
|
return this.sendError( "User has no permission for " + this._permissionID );
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve( [] );
|
return this.giveOK();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,27 +13,27 @@ export class UserIsLoggedIn extends RequestRequirement
|
||||||
|
|
||||||
if ( ! tokenData )
|
if ( ! tokenData )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "No token data" )] );
|
return this.sendError( "No token data" );
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenID = tokenData.token;
|
let tokenID = tokenData.token;
|
||||||
|
|
||||||
let session = this._handler._ums._sessions.get( tokenID );
|
let session = this.ums._sessions.get( tokenID );
|
||||||
|
|
||||||
if ( ! session )
|
if ( ! session )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "No session for token:" + tokenID )] );
|
return this.sendError( "No session for token:" + tokenID );
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = this._handler._ums.tokenDB._tokens.get( tokenID );
|
let token = this.ums.tokenDB._tokens.get( tokenID );
|
||||||
|
|
||||||
let isValid = await this._handler._ums.tokenDB.validate( token, request.ip );
|
let isValid = await this.ums.tokenDB.validate( token, request.ip );
|
||||||
|
|
||||||
if ( ! isValid )
|
if ( ! isValid )
|
||||||
{
|
{
|
||||||
return Promise.resolve( [ Message.Error( "Invalid token" + tokenID )] );
|
return this.sendError( "Invalid token" + tokenID );
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve( [] );
|
return this.giveOK();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { MinPriorityQueue } from '@datastructures-js/priority-queue';
|
||||||
|
import { Arrays } from '../../../browser/tools/Arrays';
|
||||||
|
import { DateHelper } from '../../../browser/date/DateHelper';
|
||||||
|
import { DateMath } from '../../../browser/date/DateMath';
|
||||||
|
import { Task } from './Task';
|
||||||
|
import { iTaskScheduler } from './iTaskScheduler';
|
||||||
|
import { RJLog } from '../../log/RJLog';
|
||||||
|
|
||||||
|
export class Scheduler
|
||||||
|
{
|
||||||
|
// [ Tasks ]
|
||||||
|
_queue:Task[] = [];
|
||||||
|
_scheduledTasks = new Set<string>();
|
||||||
|
|
||||||
|
// [ Next Task ]
|
||||||
|
_taskTimerCallback:NodeJS.Timeout;
|
||||||
|
_nextTaskID:string;
|
||||||
|
|
||||||
|
// [ Settings ]
|
||||||
|
_maxScheduleDurationDays:number = 1;
|
||||||
|
|
||||||
|
// [ Schedulers ]
|
||||||
|
_taskSchedulers:iTaskScheduler[] = [];
|
||||||
|
get taskSchedulers(){ return this._taskSchedulers; }
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this._queue =[];
|
||||||
|
this._taskTimerCallback = null;
|
||||||
|
this._nextTaskID = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxDate():Date
|
||||||
|
{
|
||||||
|
return DateMath.fromNowAddDays( this._maxScheduleDurationDays );
|
||||||
|
}
|
||||||
|
|
||||||
|
async update()
|
||||||
|
{
|
||||||
|
let maxDate = this.maxDate;
|
||||||
|
|
||||||
|
let allTasks:Task[] = [];
|
||||||
|
|
||||||
|
for ( let ts of this._taskSchedulers )
|
||||||
|
{
|
||||||
|
let tasks = await ts.getTasksToSchedule( maxDate );
|
||||||
|
allTasks = allTasks.concat( tasks );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleTasks( allTasks );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
scheduleTasks( tasks:Task[] )
|
||||||
|
{
|
||||||
|
let maxDate = DateMath.fromNowAddDays( this._maxScheduleDurationDays );
|
||||||
|
tasks = tasks.filter( t => ! this._scheduledTasks.has( t.id ) && ! DateMath.isAfter( t.date, maxDate ) );
|
||||||
|
|
||||||
|
|
||||||
|
this._queue = this._queue.concat( tasks );
|
||||||
|
tasks.forEach( t => this._scheduledTasks.add( t.id ) );
|
||||||
|
|
||||||
|
this._queue.sort( ( a, b ) => { return a.date.getTime() - b.date.getTime() } );
|
||||||
|
|
||||||
|
let newNextTask = this._queue[ 0 ];
|
||||||
|
|
||||||
|
if ( this._nextTaskID && newNextTask.id != this._nextTaskID )
|
||||||
|
{
|
||||||
|
this._resetTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _resetTimer()
|
||||||
|
{
|
||||||
|
if ( this._taskTimerCallback )
|
||||||
|
{
|
||||||
|
clearTimeout( this._taskTimerCallback );
|
||||||
|
this._taskTimerCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._nextTaskID = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _updateTimer()
|
||||||
|
{
|
||||||
|
// RJLog.log( "_updateTimer:" );
|
||||||
|
|
||||||
|
if ( this._taskTimerCallback || this._queue.length == 0 )
|
||||||
|
{
|
||||||
|
// RJLog.log( "Nothing to do. Has Timer:", this._taskTimerCallback, "Queue Length:", this._queue.length );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = this._queue[ 0 ];
|
||||||
|
this._nextTaskID = task.id;
|
||||||
|
|
||||||
|
let delay = Math.max( 0, task.date.getTime() - Date.now() );
|
||||||
|
|
||||||
|
RJLog.log( "delaying:", delay );
|
||||||
|
|
||||||
|
this._taskTimerCallback = setTimeout(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
this._taskTimerCallback = null;
|
||||||
|
Arrays.remove( this._queue, task );
|
||||||
|
this._scheduledTasks.delete( task.id );
|
||||||
|
this._nextTaskID = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
task.action();
|
||||||
|
}
|
||||||
|
catch( e )
|
||||||
|
{
|
||||||
|
RJLog.log( e );
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateTimer();
|
||||||
|
|
||||||
|
},
|
||||||
|
delay
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelTask( id:string ): void
|
||||||
|
{
|
||||||
|
let index = this._queue.findIndex( e => e.id === id );
|
||||||
|
|
||||||
|
if ( index === -1 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.removeAt( this._queue, index );
|
||||||
|
|
||||||
|
this._resetTimer();
|
||||||
|
this._updateTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { MinPriorityQueue } from '@datastructures-js/priority-queue';
|
||||||
|
import { Arrays } from '../../../browser/tools/Arrays';
|
||||||
|
import { DateHelper } from '../../../browser/date/DateHelper';
|
||||||
|
import { DateMath } from '../../../browser/date/DateMath';
|
||||||
|
|
||||||
|
export class Task
|
||||||
|
{
|
||||||
|
id:string;
|
||||||
|
date:Date;
|
||||||
|
action:()=>void;
|
||||||
|
|
||||||
|
static createIn( duration:number, action:()=>void )
|
||||||
|
{
|
||||||
|
let date = DateMath.fromNowAddSeconds( duration );
|
||||||
|
return Task.createAt( date, action );
|
||||||
|
}
|
||||||
|
|
||||||
|
static createAt( date:Date, action:()=>void )
|
||||||
|
{
|
||||||
|
let task = new Task();
|
||||||
|
|
||||||
|
task.id = crypto.randomUUID();
|
||||||
|
task.action = action;
|
||||||
|
task.date = date;
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Scheduler } from "./Scheduler";
|
||||||
|
import { Task } from "./Task";
|
||||||
|
|
||||||
|
export interface iTaskScheduler
|
||||||
|
{
|
||||||
|
getTasksToSchedule( maxDate:Date ):Promise<Task[]>;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue