diff --git a/browser/date/DateFormatter.ts b/browser/date/DateFormatter.ts index 30cb217..e81cba7 100644 --- a/browser/date/DateFormatter.ts +++ b/browser/date/DateFormatter.ts @@ -1,9 +1,11 @@ import { TextTool } from "../text/TextTool"; +import { DateHelper } from "./DateHelper"; 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 mo = TextTool.prependZeros( ( date.getMonth() + 1 ) ); let da = TextTool.prependZeros( date.getDate() ); @@ -15,6 +17,17 @@ export class DateFormatter 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 { let ye = ( date.getFullYear() + "" ); diff --git a/browser/date/DateHelper.ts b/browser/date/DateHelper.ts index 266e433..bd5ba2d 100644 --- a/browser/date/DateHelper.ts +++ b/browser/date/DateHelper.ts @@ -1,3 +1,6 @@ +import { DateExpressionLexer } from "../text/lexer/DateExpressionLexer"; +import { LexerQuery } from "../text/lexer/LexerQuery"; + export class DateHelper { static now() @@ -31,4 +34,62 @@ export class DateHelper 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 ); + + } } \ No newline at end of file diff --git a/browser/date/DateMath.ts b/browser/date/DateMath.ts index cd52523..5efc43b 100644 --- a/browser/date/DateMath.ts +++ b/browser/date/DateMath.ts @@ -74,7 +74,8 @@ export class DateMath 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 ) @@ -82,6 +83,11 @@ export class DateMath 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 ) { return a.getTime() - b.getTime(); @@ -115,16 +121,16 @@ export class DateMath return DateMath.addMinutes( new Date(), minutes ); } + static fromNowAddSeconds( seconds:number ) + { + return DateMath.addSeconds( new Date(), seconds ); + } + static addSeconds( a:Date, duration:number ) { return this.addMilliseconds( a, duration * 1000 ); } - static durationToHours( duration:number ) - { - - } - static addMinutes( a:Date, durationMinutes:number ) { return this.addSeconds( a, durationMinutes * 60 ); diff --git a/browser/date/Duration.ts b/browser/date/Duration.ts index 2db4fdb..5307794 100644 --- a/browser/date/Duration.ts +++ b/browser/date/Duration.ts @@ -1,5 +1,15 @@ export class Duration { + static toMilliSeconds( duration:number ) + { + return duration * 1000; + } + + static fromMilliSeconds( duration:number ) + { + return duration / 1000; + } + static toMinutes( duration:number ) { return duration / 60; diff --git a/browser/expressions/BooleanExpression.ts b/browser/expressions/BooleanExpression.ts index f4bd4fd..d4802a0 100644 --- a/browser/expressions/BooleanExpression.ts +++ b/browser/expressions/BooleanExpression.ts @@ -112,3 +112,19 @@ export class OrExpression extends ListInputExpression } } +export class LamdaExpression extends BooleanExpression +{ + _lamda:(t:T)=>boolean; + + constructor( lamda:(t:T)=>boolean ) + { + super(); + this._lamda = lamda; + } + + evaluate( t:T ) + { + return this._lamda( t ); + } +} + diff --git a/browser/random/CryptoTool.ts b/browser/random/CryptoTool.ts new file mode 100644 index 0000000..d7812f9 --- /dev/null +++ b/browser/random/CryptoTool.ts @@ -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(); + } +} \ No newline at end of file diff --git a/browser/text/lexer/DateExpressionLexer.ts b/browser/text/lexer/DateExpressionLexer.ts new file mode 100644 index 0000000..a94fe6d --- /dev/null +++ b/browser/text/lexer/DateExpressionLexer.ts @@ -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 + ); + + } + +} \ No newline at end of file diff --git a/browser/text/lexer/Lexer.ts b/browser/text/lexer/Lexer.ts index 87edcb3..cc14019 100644 --- a/browser/text/lexer/Lexer.ts +++ b/browser/text/lexer/Lexer.ts @@ -1,5 +1,6 @@ import { LexerMatcher } from "./LexerMatcher"; import { LexerEvent } from "./LexerEvent"; +import { LexerQuery } from "./LexerQuery"; export class Lexer { @@ -128,6 +129,14 @@ export class Lexer 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 ) { var list:LexerEvent[] = []; diff --git a/browser/text/lexer/LexerEvent.ts b/browser/text/lexer/LexerEvent.ts index e312234..094a9c7 100644 --- a/browser/text/lexer/LexerEvent.ts +++ b/browser/text/lexer/LexerEvent.ts @@ -1,3 +1,5 @@ +import { LexerMatcher } from "./LexerMatcher"; + export class LexerEvent { static readonly LENGTH_ERROR_ID = -1; @@ -41,7 +43,12 @@ export class LexerEvent 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; } diff --git a/browser/text/lexer/LexerQuery.ts b/browser/text/lexer/LexerQuery.ts index d9c8592..56ad3b6 100644 --- a/browser/text/lexer/LexerQuery.ts +++ b/browser/text/lexer/LexerQuery.ts @@ -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"; @@ -9,6 +11,11 @@ export class LexerQuery _index:Map = new Map(); + static from( source:string, lexer:Lexer ) + { + return lexer.createLexerQuery( source ); + } + createTokenIndex() { for ( let i = 0; i < this.tokens.length; i++ ) @@ -258,4 +265,20 @@ export class LexerQuery return nextIndex === -1 ? null : this.tokens[ nextIndex ]; } + + find( matcherOrPredicate:BooleanExpression|( (l:LexerEvent)=>boolean ) ) + { + let matcher:BooleanExpression = null; + + if ( isClassOf( matcher, BooleanExpression ) ) + { + matcher = matcherOrPredicate as BooleanExpression; + } + else + { + matcher = new LamdaExpression( matcherOrPredicate as (l:LexerEvent)=>boolean ); + } + + return this.searchItem( 0, true, matcher ); + } } \ No newline at end of file diff --git a/node/users/ErrorInfo.ts b/node/users/ErrorInfo.ts new file mode 100644 index 0000000..7f09ba4 --- /dev/null +++ b/node/users/ErrorInfo.ts @@ -0,0 +1,4 @@ +export class ErrorInfo +{ + +} \ No newline at end of file diff --git a/node/users/RequestHandler.ts b/node/users/RequestHandler.ts index 5aa360d..5a80768 100644 --- a/node/users/RequestHandler.ts +++ b/node/users/RequestHandler.ts @@ -19,6 +19,9 @@ export abstract class RequestHandler { _ums:UserManagementServer; + + get ums(){ return this._ums; }; + _type:RequestType; _url:string; diff --git a/node/users/UserDB.ts b/node/users/UserDB.ts index bfd927f..893d369 100644 --- a/node/users/UserDB.ts +++ b/node/users/UserDB.ts @@ -32,6 +32,11 @@ export class UserDB return this.users.findIndex( u => u.email === email ) != -1; } + async hasUserWithID( id:string ):Promise + { + return this.users.findIndex( u => u.id === id ) != -1; + } + async signUp( email:string, password:string, userName:string = "User" ):Promise { let userData = this._pendingUsers.find( u => u.email === email ); @@ -43,6 +48,8 @@ export class UserDB userData.email = email; userData.hashedPassword = await CryptIO.hash( password ); userData.name = userName; + userData.role = Role.User; + userData.permissions = []; this._pendingUsers.push( userData ); @@ -208,8 +215,10 @@ export class UserDB return Promise.resolve( false ); } - async _hasPermission( permissionID:string, permissions:Permission[] ):Promise + protected async _hasPermission( permissionID:string, permissions:Permission[] ):Promise { + permissions = permissions || []; + for ( let p of permissions ) { if ( Permission.isMatching( permissionID, p ) ) diff --git a/node/users/UserManagementServer.ts b/node/users/UserManagementServer.ts index 1c40cf0..abc6944 100644 --- a/node/users/UserManagementServer.ts +++ b/node/users/UserManagementServer.ts @@ -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 cors from '@fastify/cors'; +import { EventSlot } from "../../browser/events/EventSlot"; +import { JSRandomEngine } from "../../browser/random/JSRandomEngine"; + import { UserDB } from "./UserDB"; import { RequestHandler } from "./RequestHandler"; import { EmailService as EmailService } from "./email/EmailService"; @@ -16,6 +19,15 @@ import { LocationService } from "./location/LocationService"; import { RequestRequirement } from "./requirements/RequestRequirement"; import { NotTooManyRequests } from "./requirements/security/NotTooManyRequests"; 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 { @@ -43,6 +55,14 @@ export class UserManagementServer _globalRequirements:RequestRequirement[] = []; get globalRequirements(){ return this._globalRequirements; } + _apps:UserApp[] = []; + get apps(){ return this._apps; } + + _scheduler:Scheduler; + get scheduler(){ return this._scheduler; } + + readonly onFileChanged = new EventSlot(); + async initialize( settings:UserManagementServerSettings, mailService:EmailService, handlers:RequestHandler[], globalRequirements:RequestRequirement[] = null ):Promise { @@ -54,9 +74,12 @@ export class UserManagementServer await this._addGlobalRequirements( globalRequirements || UserManagementServer.DefaultGlobalRequirements() ); await this._addServices( mailService ); await this._addHandlers( handlers ); + await this._startApps(); await this._startServer(); + this._update(); + return Promise.resolve(); } @@ -83,6 +106,16 @@ export class UserManagementServer return Promise.resolve(); } + getUser( request:FastifyRequest ):Promise + { + 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 { return this.email.send( this._settings.emailFrom, to, title, message ); @@ -115,27 +148,52 @@ export class UserManagementServer 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, - { - origin: corsURL, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] - } - ); + { + origin: "*", + }); + } + 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(); + } + + _updateDuration = 60; + + protected _update() + { + let self = this; + + let waitTimeMS = Duration.toMilliSeconds( this._updateDuration ); + + setTimeout( () =>{ self._update() }, waitTimeMS ); } async _addServices( mailService:EmailService ):Promise { 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( this._settings.rolesPath ); this._roles = new Map(); rolesData.roles.forEach( r => this._roles.set( r.id, r ) ); @@ -177,10 +235,10 @@ export class UserManagementServer port: this._settings.port } - if ( ! this._settings.isDebugMode ) - { + // if ( ! this._settings.isDebugMode ) + // { listenOptions.host = "0.0.0.0"; - } + // } 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(); + } } \ No newline at end of file diff --git a/node/users/UserManagementServerSettings.ts b/node/users/UserManagementServerSettings.ts index 0f4b263..4ce6f91 100644 --- a/node/users/UserManagementServerSettings.ts +++ b/node/users/UserManagementServerSettings.ts @@ -17,6 +17,10 @@ export class UserManagementServerSettings userDBPath:string; rolesPath:string; + // Apps + appsPath:string; + userApps:string[]; + // Geo-Lite Lib geoLocationPath:string; geoAccountID:string; diff --git a/node/users/apps/UserApp.ts b/node/users/apps/UserApp.ts new file mode 100644 index 0000000..36edb99 --- /dev/null +++ b/node/users/apps/UserApp.ts @@ -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 +{ + 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(); + + constructor( id:string, ums:UserManagementServer ) + { + this._ums = ums; + this._id = id; + + this._path = Files.joinPaths( [ this._ums._settings.appsPath, this._id ] ); + } + + async initialize():Promise + { + 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):Promise + { + 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( file.absolutePath ); + + return action( userData, appData ); + } + ); + } +} \ No newline at end of file diff --git a/node/users/apps/UserAppFactory.ts b/node/users/apps/UserAppFactory.ts new file mode 100644 index 0000000..018d418 --- /dev/null +++ b/node/users/apps/UserAppFactory.ts @@ -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 + { + let constructors = [ ReminderApp ]; + + for ( let c of constructors ) + { + if ( c.id === id ) + { + return new c( ums ); + } + } + + return null; + }; +} \ No newline at end of file diff --git a/node/users/apps/reminder/ReminderApp.ts b/node/users/apps/reminder/ReminderApp.ts new file mode 100644 index 0000000..6bd03ef --- /dev/null +++ b/node/users/apps/reminder/ReminderApp.ts @@ -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 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(); + + let tasks:Task[] =[]; + this.grabTasks( user, data, this.ums.scheduler.maxDate, tasks ); + this.ums.scheduler.scheduleTasks( tasks ); + + } + ) + } + + async getTasksToSchedule( maxDate:Date ):Promise + { + 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 ) + } + } + ); + } +} \ No newline at end of file diff --git a/node/users/apps/reminder/data/MailEntry.ts b/node/users/apps/reminder/data/MailEntry.ts new file mode 100644 index 0000000..2a49369 --- /dev/null +++ b/node/users/apps/reminder/data/MailEntry.ts @@ -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; + } + + +} \ No newline at end of file diff --git a/node/users/apps/reminder/data/ReminderData.ts b/node/users/apps/reminder/data/ReminderData.ts new file mode 100644 index 0000000..25785af --- /dev/null +++ b/node/users/apps/reminder/data/ReminderData.ts @@ -0,0 +1,13 @@ +import { MailEntry } from "./MailEntry"; + +export class ReminderData +{ + mailEntries:MailEntry[]; + + static create() + { + let rd = new ReminderData(); + rd.mailEntries = []; + return rd; + } +} \ No newline at end of file diff --git a/node/users/apps/reminder/data/ReminderEntry.ts b/node/users/apps/reminder/data/ReminderEntry.ts new file mode 100644 index 0000000..b3f4062 --- /dev/null +++ b/node/users/apps/reminder/data/ReminderEntry.ts @@ -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(); + } +} \ No newline at end of file diff --git a/node/users/handlers/HandlerGroups.ts b/node/users/handlers/HandlerGroups.ts index 24e5e81..e4b0898 100644 --- a/node/users/handlers/HandlerGroups.ts +++ b/node/users/handlers/HandlerGroups.ts @@ -6,6 +6,9 @@ import { LoginHandler } from "./_/login"; import { LogoutHandler } from "./_/logout"; import { RequestPasswordChangeHandler } from "./_/request-password-change"; import { SignUpHandler } from "./_/signup"; +import { AppsCanUseHandler } from "./apps/can-use"; +import { AppsLoadHandler } from "./apps/load"; +import { AppsSaveHandler } from "./apps/save"; export class HandlerGroups { @@ -21,7 +24,12 @@ export class HandlerGroups new InfoHandler(), new RequestPasswordChangeHandler(), - new ChangePasswordHandler() + new ChangePasswordHandler(), + + new AppsCanUseHandler(), + new AppsSaveHandler(), + new AppsLoadHandler(), + ]; return defaultHandlers; diff --git a/node/users/handlers/_/confirm-signup.ts b/node/users/handlers/_/confirm-signup.ts index 3e1fd56..801e399 100644 --- a/node/users/handlers/_/confirm-signup.ts +++ b/node/users/handlers/_/confirm-signup.ts @@ -21,7 +21,7 @@ export class ConfirmSignUpHandler extends RequestHandler if ( ! result ) { - return this.sendError( "User signup confimration failed" ); + return this.sendError( "User signup confirmation failed" ); } return this.sendInfo( "User creation confirmed" ); diff --git a/node/users/handlers/apps/UserCanUseApp.ts b/node/users/handlers/apps/UserCanUseApp.ts new file mode 100644 index 0000000..4b867c8 --- /dev/null +++ b/node/users/handlers/apps/UserCanUseApp.ts @@ -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 + { + 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(); + } +} diff --git a/node/users/handlers/apps/can-use.ts b/node/users/handlers/apps/can-use.ts new file mode 100644 index 0000000..9182759 --- /dev/null +++ b/node/users/handlers/apps/can-use.ts @@ -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" ); + } +} \ No newline at end of file diff --git a/node/users/handlers/apps/load.ts b/node/users/handlers/apps/load.ts new file mode 100644 index 0000000..4c642d8 --- /dev/null +++ b/node/users/handlers/apps/load.ts @@ -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( filePath ); + + return this.sendDataInfo( "Data loaded", data ); + } +} \ No newline at end of file diff --git a/node/users/handlers/apps/save.ts b/node/users/handlers/apps/save.ts new file mode 100644 index 0000000..3dbec7b --- /dev/null +++ b/node/users/handlers/apps/save.ts @@ -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" ); + } +} \ No newline at end of file diff --git a/node/users/permissions/Role.ts b/node/users/permissions/Role.ts index 5ee4897..27d215a 100644 --- a/node/users/permissions/Role.ts +++ b/node/users/permissions/Role.ts @@ -2,6 +2,9 @@ import { Permission } from "./Permission"; export class Role { + static readonly User = "user"; + static readonly Admin = "admin"; + id:string; inherits:string; permissions:Permission[]; diff --git a/node/users/requirements/RequestRequirement.ts b/node/users/requirements/RequestRequirement.ts index 5a19a05..9977ada 100644 --- a/node/users/requirements/RequestRequirement.ts +++ b/node/users/requirements/RequestRequirement.ts @@ -2,12 +2,16 @@ import { RequestHandler } from "../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; import { UserManagementServer } from "../UserManagementServer"; import { Message } from "../../../browser/messages/Message"; +import { ErrorInfo } from "../ErrorInfo"; +import { MessageTypes } from "../../../browser/messages/MessageType"; export abstract class RequestRequirement { - _isGlobal:boolean = false; - _ums:UserManagementServer; - _handler:RequestHandler = null; + private _isGlobal:boolean = false; + private _ums:UserManagementServer; + private _handler:RequestHandler = null; + get handler(){ return this._handler; } + get ums() { return this._isGlobal ? this._ums : this._handler._ums }; initialize( handler:RequestHandler ):Promise @@ -25,6 +29,17 @@ export abstract class RequestRequirement } + sendError( message:string, errorInfo:ErrorInfo = undefined ):Promise + { + let messages = [ Message.Error( message ) ]; + return Promise.resolve( messages ); + } + + giveOK():Promise + { + return Promise.resolve( [] ); + } + abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise } \ No newline at end of file diff --git a/node/users/requirements/security/NotTooManyRequests.ts b/node/users/requirements/security/NotTooManyRequests.ts index 8d38ee5..86b265f 100644 --- a/node/users/requirements/security/NotTooManyRequests.ts +++ b/node/users/requirements/security/NotTooManyRequests.ts @@ -26,7 +26,7 @@ export class NotTooManyRequests extends RequestRequirement 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 ) { - 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 diff --git a/node/users/requirements/user/UserHasPermission.ts b/node/users/requirements/user/UserHasPermission.ts index 13606f2..d9bb002 100644 --- a/node/users/requirements/user/UserHasPermission.ts +++ b/node/users/requirements/user/UserHasPermission.ts @@ -15,17 +15,17 @@ export class UserHasPermission extends RequestRequirement async handle( request:FastifyRequest, reply:FastifyReply ):Promise { - 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 ); 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(); } } \ No newline at end of file diff --git a/node/users/requirements/user/UserIsLoggedIn.ts b/node/users/requirements/user/UserIsLoggedIn.ts index 802cc66..52b903c 100644 --- a/node/users/requirements/user/UserIsLoggedIn.ts +++ b/node/users/requirements/user/UserIsLoggedIn.ts @@ -13,27 +13,27 @@ export class UserIsLoggedIn extends RequestRequirement if ( ! tokenData ) { - return Promise.resolve( [ Message.Error( "No token data" )] ); + return this.sendError( "No token data" ); } let tokenID = tokenData.token; - let session = this._handler._ums._sessions.get( tokenID ); + let session = this.ums._sessions.get( tokenID ); 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 ) { - return Promise.resolve( [ Message.Error( "Invalid token" + tokenID )] ); + return this.sendError( "Invalid token" + tokenID ); } - return Promise.resolve( [] ); + return this.giveOK(); } } diff --git a/node/users/scheduler/Scheduler.ts b/node/users/scheduler/Scheduler.ts new file mode 100644 index 0000000..7cb54d1 --- /dev/null +++ b/node/users/scheduler/Scheduler.ts @@ -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(); + + // [ 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(); + } +} \ No newline at end of file diff --git a/node/users/scheduler/Task.ts b/node/users/scheduler/Task.ts new file mode 100644 index 0000000..c1a0469 --- /dev/null +++ b/node/users/scheduler/Task.ts @@ -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; + } +} \ No newline at end of file diff --git a/node/users/scheduler/iTaskScheduler.ts b/node/users/scheduler/iTaskScheduler.ts new file mode 100644 index 0000000..42c12a6 --- /dev/null +++ b/node/users/scheduler/iTaskScheduler.ts @@ -0,0 +1,7 @@ +import { Scheduler } from "./Scheduler"; +import { Task } from "./Task"; + +export interface iTaskScheduler +{ + getTasksToSchedule( maxDate:Date ):Promise; +} \ No newline at end of file