Add Reminder App

This commit is contained in:
Josef 2025-11-15 19:58:30 +01:00
parent 5efc796edc
commit 2046fdf342
35 changed files with 927 additions and 43 deletions

View File

@ -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() + "" );

View File

@ -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 );
}
}

View File

@ -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 );

View File

@ -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;

View File

@ -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 );
}
}

View File

@ -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();
}
}

View File

@ -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
);
}
}

View File

@ -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[] = [];

View File

@ -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; }

View File

@ -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<LexerEvent,number> = new Map<LexerEvent,number>();
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<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 );
}
}

4
node/users/ErrorInfo.ts Normal file
View File

@ -0,0 +1,4 @@
export class ErrorInfo
{
}

View File

@ -19,6 +19,9 @@ export abstract class RequestHandler
{
_ums:UserManagementServer;
get ums(){ return this._ums; };
_type:RequestType;
_url:string;

View File

@ -32,6 +32,11 @@ export class UserDB
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>
{
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<boolean>
protected async _hasPermission( permissionID:string, permissions:Permission[] ):Promise<boolean>
{
permissions = permissions || [];
for ( let p of permissions )
{
if ( Permission.isMatching( permissionID, p ) )

View File

@ -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<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>
{
@ -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<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>
{
return this.email.send( this._settings.emailFrom, to, title, message );
@ -115,6 +148,17 @@ export class UserManagementServer
this._app.register( fastifyMultipart );
if ( this._settings.isDebugMode )
{
RJLog.log( "Setting any cors:" );
await this._app.register( cors,
{
origin: "*",
});
}
else
{
for ( let corsURL of this._settings.corsURLs )
{
RJLog.log( "Adding cors:", corsURL );
@ -127,15 +171,29 @@ export class UserManagementServer
}
);
}
}
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>
{
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 );
this._roles = new Map<string,Role>();
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();
}
}

View File

@ -17,6 +17,10 @@ export class UserManagementServerSettings
userDBPath:string;
rolesPath:string;
// Apps
appsPath:string;
userApps:string[];
// Geo-Lite Lib
geoLocationPath:string;
geoAccountID:string;

View File

@ -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 );
}
);
}
}

View File

@ -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;
};
}

View File

@ -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 )
}
}
);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,13 @@
import { MailEntry } from "./MailEntry";
export class ReminderData
{
mailEntries:MailEntry[];
static create()
{
let rd = new ReminderData();
rd.mailEntries = [];
return rd;
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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" );

View File

@ -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();
}
}

View File

@ -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" );
}
}

View File

@ -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 );
}
}

View File

@ -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" );
}
}

View File

@ -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[];

View File

@ -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<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[]>
}

View File

@ -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<boolean>

View File

@ -15,17 +15,17 @@ export class UserHasPermission extends RequestRequirement
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 );
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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
import { Scheduler } from "./Scheduler";
import { Task } from "./Task";
export interface iTaskScheduler
{
getTasksToSchedule( maxDate:Date ):Promise<Task[]>;
}