diff --git a/browser/messages/Message.ts b/browser/messages/Message.ts index 60dd36e..c1441c2 100644 --- a/browser/messages/Message.ts +++ b/browser/messages/Message.ts @@ -5,6 +5,18 @@ export class Message type:MessageType; content:string; + static hasError( messages:Message[] ):boolean + { + if ( ! messages || messages.length == 0 ) + { + return false; + } + + let errorMessage = messages.find( m => MessageTypes.Error == m.type ); + + return ! ( ! errorMessage ); + } + static with( type:MessageType, content:string ) { let message = new Message(); diff --git a/node/users/RequestHandler.ts b/node/users/RequestHandler.ts index 673994b..5aa360d 100644 --- a/node/users/RequestHandler.ts +++ b/node/users/RequestHandler.ts @@ -7,6 +7,7 @@ import { Message } from "../../browser/messages/Message"; import { UserData } from "./UserData"; import { UAParser } from 'ua-parser-js'; import { LocationData } from "./location/LocationData"; +import { className } from "../../browser/tools/TypeUtilities"; export enum RequestType { @@ -85,11 +86,11 @@ export abstract class RequestHandler for ( let r of this._ums.globalRequirements ) { - let fullfilled = await r.handle( request, reply ); + let messages = await r.handle( request, reply ); - if ( ! fullfilled ) + if ( Message.hasError( messages ) ) { - RJLog.log( "Global Requirement not fullfilled", r ); + RJLog.log( "Global Requirement not fullfilled: ", className( r ), messages.map( m => JSON.stringify( m ) ).join( ", " ) ); return this.sendError( "Error during global requirements check" ); } @@ -97,11 +98,12 @@ export abstract class RequestHandler for ( let r of this.requirements ) { - let fullfilled = await r.handle( request, reply ); + let messages = await r.handle( request, reply ); - if ( ! fullfilled ) + if ( Message.hasError( messages ) ) { - RJLog.log( "Requirement not fullfilled", r ); + RJLog.log( messages[ 0 ].content ); + RJLog.log( "Requirement not fullfilled: ", className( r ), messages.map( m => JSON.stringify( m ) ).join( ", " ) ); return this.sendError( "Error during requirements check" ); } diff --git a/node/users/UserDB.ts b/node/users/UserDB.ts index 92d1cf9..bfd927f 100644 --- a/node/users/UserDB.ts +++ b/node/users/UserDB.ts @@ -22,6 +22,7 @@ export class UserDB _pendingUsers:UserData[] = []; _signUpTokens = new Map(); + _passwordChangeTokens = new Map(); @@ -62,6 +63,15 @@ export class UserDB return Promise.resolve( token ); } + async createPasswordChange( userData:UserData, ip:string ):Promise + { + let token = await this._ums.tokenDB.create( ip ); + + this._passwordChangeTokens.set( userData.id, token ); + + return Promise.resolve( token ); + } + async byID( id:string ) { return this.users.find( u => u.id === id ); @@ -98,6 +108,8 @@ export class UserDB if ( isValid ) { Arrays.remove( this._pendingUsers, user ); + this._signUpTokens.delete( userID ); + this._ums.tokenDB._tokens.delete( token.id ); this.users.push( user ); await this.save(); @@ -107,6 +119,37 @@ export class UserDB return Promise.resolve( isValid ); } + async changePassword( userID:string, tokenID:string, password:string, ip:string ):Promise + { + if ( ! this._passwordChangeTokens.has( userID ) ) + { + return Promise.resolve( false ); + } + + let token = this._passwordChangeTokens.get( userID ); + + if ( token.id != tokenID ) + { + return Promise.resolve( false ); + } + + let isValid = await this._ums.tokenDB.validate( token, ip ); + + if ( isValid ) + { + this._passwordChangeTokens.delete( userID ); + this._ums.tokenDB._tokens.delete( token.id ); + let userData = await this._ums.userDB.byID( userID ); + userData.hashedPassword = await CryptIO.hash( password ); + + await this.save(); + + } + + return Promise.resolve( isValid ); + } + + async login( email:string, password:string, ip:string ):Promise { let user = await this.byEmail( email ); diff --git a/node/users/UserManagementServerSettings.ts b/node/users/UserManagementServerSettings.ts index eb9cbbb..0f4b263 100644 --- a/node/users/UserManagementServerSettings.ts +++ b/node/users/UserManagementServerSettings.ts @@ -27,6 +27,8 @@ export class UserManagementServerSettings // User settings maxLogins:number = 5; + signUpConfirmationURL:string; + changePasswordURL:string; static makeAllPathsAbsolute( settings:UserManagementServerSettings ) diff --git a/node/users/handlers/HandlerGroups.ts b/node/users/handlers/HandlerGroups.ts new file mode 100644 index 0000000..24e5e81 --- /dev/null +++ b/node/users/handlers/HandlerGroups.ts @@ -0,0 +1,29 @@ +import { RequestHandler } from "../RequestHandler"; +import { ChangePasswordHandler } from "./_/change-password"; +import { ConfirmSignUpHandler } from "./_/confirm-signup"; +import { InfoHandler } from "./_/info"; +import { LoginHandler } from "./_/login"; +import { LogoutHandler } from "./_/logout"; +import { RequestPasswordChangeHandler } from "./_/request-password-change"; +import { SignUpHandler } from "./_/signup"; + +export class HandlerGroups +{ + static get UserDefaultHandlers():RequestHandler[] + { + let defaultHandlers:RequestHandler[] = + [ + new SignUpHandler(), + new ConfirmSignUpHandler(), + + new LoginHandler(), + new LogoutHandler(), + new InfoHandler(), + + new RequestPasswordChangeHandler(), + new ChangePasswordHandler() + ]; + + return defaultHandlers; + } +} \ No newline at end of file diff --git a/node/users/handlers/_/change-password.ts b/node/users/handlers/_/change-password.ts new file mode 100644 index 0000000..5802aff --- /dev/null +++ b/node/users/handlers/_/change-password.ts @@ -0,0 +1,33 @@ +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; +import { FastifyRequest, FastifyReply } from 'fastify'; + +export class ChangePasswordHandler extends RequestHandler +{ + static url = "/change-password"; + constructor(){ super( RequestType.POST, ChangePasswordHandler.url ); } + + async _handle( request:FastifyRequest, reply:FastifyReply ) + { + let requestBody = request.body; + let { id, password, token } = requestBody as { id: string; password:string, token: string, }; + + if ( ! id || ! token || ! password) + { + return this.sendError( "Missing id, token or password" ); + } + + let result = await this.userDB.changePassword( id, token, password, this.ip ); + + if ( ! result ) + { + return this.sendError( "Password change failed" ); + } + + return this.sendInfo( "Changed password" ); + } + + + +} \ No newline at end of file diff --git a/node/users/handlers/confirm-signup.ts b/node/users/handlers/_/confirm-signup.ts similarity index 81% rename from node/users/handlers/confirm-signup.ts rename to node/users/handlers/_/confirm-signup.ts index 5885480..3e1fd56 100644 --- a/node/users/handlers/confirm-signup.ts +++ b/node/users/handlers/_/confirm-signup.ts @@ -1,6 +1,6 @@ -import { CryptIO } from "../../crypt/CryptIO"; -import { RJLog } from "../../log/RJLog"; -import { RequestHandler, RequestType } from "../RequestHandler"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; export class ConfirmSignUpHandler extends RequestHandler diff --git a/node/users/handlers/info.ts b/node/users/handlers/_/info.ts similarity index 64% rename from node/users/handlers/info.ts rename to node/users/handlers/_/info.ts index 3a8476a..cb482a5 100644 --- a/node/users/handlers/info.ts +++ b/node/users/handlers/_/info.ts @@ -1,16 +1,16 @@ -import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; -import { CryptIO } from "../../crypt/CryptIO"; -import { RJLog } from "../../log/RJLog"; -import { RequestHandler, RequestType } from "../RequestHandler"; +import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; -import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer"; +import { VariableReplacer, Variables } from "../../../../browser/text/replacing/VariableReplacer"; import { ConfirmSignUpHandler } from "./confirm-signup"; -import { Session } from "../Session"; -import { LocationData } from "../location/LocationData"; -import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp"; -import { DateFormatter } from "../../../browser/date/DateFormatter"; +import { Session } from "../../Session"; +import { LocationData } from "../../location/LocationData"; +import { ISOTimeStamp } from "../../../../browser/date/ISOTimeStamp"; +import { DateFormatter } from "../../../../browser/date/DateFormatter"; import { UAParser } from "ua-parser-js"; -import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn"; +import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn"; export class UserLoginDataInfo { @@ -56,7 +56,7 @@ export class InfoHandler extends RequestHandler } ); - let info = { name:user.name, email:user.email, lastLogins:lastLogins }; + let info = { name:user.name, id:user.id, email:user.email, lastLogins:lastLogins }; return this.sendDataInfo( "User Info", info ); } diff --git a/node/users/handlers/login.ts b/node/users/handlers/_/login.ts similarity index 72% rename from node/users/handlers/login.ts rename to node/users/handlers/_/login.ts index ce0a098..eaeaae5 100644 --- a/node/users/handlers/login.ts +++ b/node/users/handlers/_/login.ts @@ -1,14 +1,14 @@ -import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; -import { CryptIO } from "../../crypt/CryptIO"; -import { RJLog } from "../../log/RJLog"; -import { RequestHandler, RequestType } from "../RequestHandler"; +import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; -import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer"; +import { VariableReplacer, Variables } from "../../../../browser/text/replacing/VariableReplacer"; import { ConfirmSignUpHandler } from "./confirm-signup"; -import { Session } from "../Session"; -import { UserLoginData } from "../UserData"; -import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp"; -import { Arrays } from "../../../browser/tools/Arrays"; +import { Session } from "../../Session"; +import { UserLoginData } from "../../UserData"; +import { ISOTimeStamp } from "../../../../browser/date/ISOTimeStamp"; +import { Arrays } from "../../../../browser/tools/Arrays"; export class LoginHandler extends RequestHandler { diff --git a/node/users/handlers/logout.ts b/node/users/handlers/_/logout.ts similarity index 58% rename from node/users/handlers/logout.ts rename to node/users/handlers/_/logout.ts index ec5415e..6458f53 100644 --- a/node/users/handlers/logout.ts +++ b/node/users/handlers/_/logout.ts @@ -1,12 +1,12 @@ -import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; -import { CryptIO } from "../../crypt/CryptIO"; -import { RJLog } from "../../log/RJLog"; -import { RequestHandler, RequestType } from "../RequestHandler"; +import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; -import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer"; +import { VariableReplacer, Variables } from "../../../../browser/text/replacing/VariableReplacer"; import { ConfirmSignUpHandler } from "./confirm-signup"; -import { Session } from "../Session"; -import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn"; +import { Session } from "../../Session"; +import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn"; export class LogoutHandler extends RequestHandler { diff --git a/node/users/handlers/_/request-password-change.ts b/node/users/handlers/_/request-password-change.ts new file mode 100644 index 0000000..bba3a86 --- /dev/null +++ b/node/users/handlers/_/request-password-change.ts @@ -0,0 +1,76 @@ +import { VariableReplacer, Variables } from "../../../../browser/text/replacing/VariableReplacer"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; +import { FastifyRequest, FastifyReply } from 'fastify'; + +export class RequestPasswordChangeHandler extends RequestHandler +{ + static url = "/request-password-change"; + constructor(){ super( RequestType.POST, RequestPasswordChangeHandler.url ); } + + static USER_NAME = "USER_NAME"; + static CHANGE_PASSWORD_LINK = "CONFIRMATION_LINK"; + + static EMAIL_TITLE = "Password Change Request"; + static EMAIL_MESSAGE = + ` + Hi {{${RequestPasswordChangeHandler.USER_NAME}}}! +
+ You requested to change your password, here is your link: +
+ CHANGE PASSWORD +
+
+ Cheers! + + ` + + async _handle( request:FastifyRequest, reply:FastifyReply ) + { + let requestBody = request.body; + let { email } = requestBody as { email:string }; + + if ( ! email ) + { + return this.sendError( "Missing email" ); + } + + let userData = await this.userDB.byEmail( email ); + + if ( ! userData ) + { + return this.sendError( "Email not found" ); + } + + let token = await this.userDB.createPasswordChange( userData, this.ip ); + + let changePasswordURL = this._ums._settings.changePasswordURL; + + let id = userData.id; + + let link = `${changePasswordURL}?id=${id}&token=${token.id}`; + + await this.sendPasswordChangeEmail( email, userData.name, link ); + + return this.sendInfo( "Send password change email" ); + } + + async sendPasswordChangeEmail( email:string, userName:string, link:string ):Promise + { + + let variables:Variables = { + [ RequestPasswordChangeHandler.USER_NAME ] : userName, + [ RequestPasswordChangeHandler.CHANGE_PASSWORD_LINK ] : link + }; + + let message = VariableReplacer.replace( RequestPasswordChangeHandler.EMAIL_MESSAGE, variables, "{{", "}}" ); + let title = VariableReplacer.replace( RequestPasswordChangeHandler.EMAIL_TITLE, variables, "{{", "}}" ); + + await this._ums.sendEmail( email, title, message ); + + return Promise.resolve(); + } + + +} \ No newline at end of file diff --git a/node/users/handlers/signup.ts b/node/users/handlers/_/signup.ts similarity index 78% rename from node/users/handlers/signup.ts rename to node/users/handlers/_/signup.ts index 73896d2..8fd7f8b 100644 --- a/node/users/handlers/signup.ts +++ b/node/users/handlers/_/signup.ts @@ -1,9 +1,9 @@ -import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; -import { CryptIO } from "../../crypt/CryptIO"; -import { RJLog } from "../../log/RJLog"; -import { RequestHandler, RequestType } from "../RequestHandler"; +import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity"; +import { CryptIO } from "../../../crypt/CryptIO"; +import { RJLog } from "../../../log/RJLog"; +import { RequestHandler, RequestType } from "../../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; -import { VariableReplacer, Variables } from "../../../browser/text/replacing/VariableReplacer"; +import { VariableReplacer, Variables } from "../../../../browser/text/replacing/VariableReplacer"; import { ConfirmSignUpHandler } from "./confirm-signup"; export class SignUpHandler extends RequestHandler @@ -55,19 +55,19 @@ export class SignUpHandler extends RequestHandler let token = await this.userDB.createSignUpConfirmation( userData, this.ip ); - let confirmationURL = this._ums._settings.url + ConfirmSignUpHandler.url; + let confirmationURL = this._ums._settings.signUpConfirmationURL; let id = userData.id; let link = `${confirmationURL}?id=${id}&token=${token.id}`; - await this.sendEmail( email, userName, link ); + await this.sendSignUpEmail( email, userName, link ); return this.sendInfo( "User created, email sent" ); } - async sendEmail( email:string, userName:string, link:string ):Promise + async sendSignUpEmail( email:string, userName:string, link:string ):Promise { let variables:Variables = { diff --git a/node/users/requirements/RequestRequirement.ts b/node/users/requirements/RequestRequirement.ts index 8880c66..5a19a05 100644 --- a/node/users/requirements/RequestRequirement.ts +++ b/node/users/requirements/RequestRequirement.ts @@ -1,6 +1,7 @@ import { RequestHandler } from "../RequestHandler"; import { FastifyRequest, FastifyReply } from 'fastify'; import { UserManagementServer } from "../UserManagementServer"; +import { Message } from "../../../browser/messages/Message"; export abstract class RequestRequirement { @@ -24,6 +25,6 @@ export abstract class RequestRequirement } - abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise + 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 5672e77..8d38ee5 100644 --- a/node/users/requirements/security/NotTooManyRequests.ts +++ b/node/users/requirements/security/NotTooManyRequests.ts @@ -5,6 +5,7 @@ import { MapList } from "../../../../browser/tools/MapList"; import { DateHelper } from "../../../../browser/date/DateHelper"; import { Duration } from "../../../../browser/date/Duration"; import { DateMath } from "../../../../browser/date/DateMath"; +import { Message } from "../../../../browser/messages/Message"; export class NotTooManyRequests extends RequestRequirement @@ -17,7 +18,7 @@ export class NotTooManyRequests extends RequestRequirement _blockList = new Map() _blockDuration = Duration.fromHours( 4 ); - async handle( request:FastifyRequest, reply:FastifyReply ):Promise + async handle( request:FastifyRequest, reply:FastifyReply ):Promise { if ( this._blockList.has( request.ip ) ) { @@ -25,13 +26,18 @@ export class NotTooManyRequests extends RequestRequirement if ( ! DateMath.isExpired( new Date( blockTime + this._blockDuration ) ) ) { - return Promise.resolve( false ); + return Promise.resolve( [ Message.Error( "User blocked" ) ] ); } } let valid = this.updateIP( request.ip ); - return Promise.resolve( valid ); + if ( ! valid ) + { + return Promise.resolve( [ Message.Error( "Too many requests" ) ] ); + } + + return Promise.resolve( [] ); } async updateIP( ip:string ):Promise diff --git a/node/users/requirements/user/UserHasPermission.ts b/node/users/requirements/user/UserHasPermission.ts index 847f2b8..13606f2 100644 --- a/node/users/requirements/user/UserHasPermission.ts +++ b/node/users/requirements/user/UserHasPermission.ts @@ -1,6 +1,7 @@ import { FastifyRequest, FastifyReply } from 'fastify'; import { RequestRequirement } from '../RequestRequirement'; +import { Message } from '../../../../browser/messages/Message'; export class UserHasPermission extends RequestRequirement { @@ -12,12 +13,19 @@ export class UserHasPermission extends RequestRequirement this._permissionID = permissionID; } - async handle( request:FastifyRequest, reply:FastifyReply ):Promise + async handle( request:FastifyRequest, reply:FastifyReply ):Promise { let user = await this._handler.getUser(); let userDB = this._handler.userDB; - return userDB.hasPermission( user, this._permissionID ); + let hasPermission = await userDB.hasPermission( user, this._permissionID ); + + if ( ! hasPermission ) + { + return Promise.resolve( [ Message.Error( "User has no permission for " + this._permissionID ) ] ); + } + + return Promise.resolve( [] ); } } \ No newline at end of file diff --git a/node/users/requirements/user/UserIsLoggedIn.ts b/node/users/requirements/user/UserIsLoggedIn.ts index 8117f5b..802cc66 100644 --- a/node/users/requirements/user/UserIsLoggedIn.ts +++ b/node/users/requirements/user/UserIsLoggedIn.ts @@ -1,18 +1,19 @@ import { FastifyRequest, FastifyReply } from 'fastify'; import { RequestRequirement } from '../RequestRequirement'; +import { Message } from '../../../../browser/messages/Message'; export class UserIsLoggedIn extends RequestRequirement { - async handle( request:FastifyRequest, reply:FastifyReply ):Promise + async handle( request:FastifyRequest, reply:FastifyReply ):Promise { let requestBody = request.body; let tokenData = requestBody as { token:string }; if ( ! tokenData ) { - return Promise.resolve( false ); + return Promise.resolve( [ Message.Error( "No token data" )] ); } let tokenID = tokenData.token; @@ -21,7 +22,7 @@ export class UserIsLoggedIn extends RequestRequirement if ( ! session ) { - return Promise.resolve( false ); + return Promise.resolve( [ Message.Error( "No session for token:" + tokenID )] ); } let token = this._handler._ums.tokenDB._tokens.get( tokenID ); @@ -30,9 +31,9 @@ export class UserIsLoggedIn extends RequestRequirement if ( ! isValid ) { - return Promise.resolve( false ); + return Promise.resolve( [ Message.Error( "Invalid token" + tokenID )] ); } - return Promise.resolve( true ); + return Promise.resolve( [] ); } }