Added Password Chagne

This commit is contained in:
Josef 2025-11-11 22:46:18 +01:00
parent 00b3020800
commit 5efc796edc
16 changed files with 268 additions and 55 deletions

View File

@ -5,6 +5,18 @@ export class Message
type:MessageType; type:MessageType;
content:string; 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 ) static with( type:MessageType, content:string )
{ {
let message = new Message(); let message = new Message();

View File

@ -7,6 +7,7 @@ import { Message } from "../../browser/messages/Message";
import { UserData } from "./UserData"; import { UserData } from "./UserData";
import { UAParser } from 'ua-parser-js'; import { UAParser } from 'ua-parser-js';
import { LocationData } from "./location/LocationData"; import { LocationData } from "./location/LocationData";
import { className } from "../../browser/tools/TypeUtilities";
export enum RequestType export enum RequestType
{ {
@ -85,11 +86,11 @@ export abstract class RequestHandler
for ( let r of this._ums.globalRequirements ) 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" ); return this.sendError( "Error during global requirements check" );
} }
@ -97,11 +98,12 @@ export abstract class RequestHandler
for ( let r of this.requirements ) 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" ); return this.sendError( "Error during requirements check" );
} }

View File

@ -22,6 +22,7 @@ export class UserDB
_pendingUsers:UserData[] = []; _pendingUsers:UserData[] = [];
_signUpTokens = new Map<string,Token>(); _signUpTokens = new Map<string,Token>();
_passwordChangeTokens = new Map<string,Token>();
@ -62,6 +63,15 @@ export class UserDB
return Promise.resolve( token ); return Promise.resolve( token );
} }
async createPasswordChange( userData:UserData, ip:string ):Promise<Token>
{
let token = await this._ums.tokenDB.create( ip );
this._passwordChangeTokens.set( userData.id, token );
return Promise.resolve( token );
}
async byID( id:string ) async byID( id:string )
{ {
return this.users.find( u => u.id === id ); return this.users.find( u => u.id === id );
@ -98,6 +108,8 @@ export class UserDB
if ( isValid ) if ( isValid )
{ {
Arrays.remove( this._pendingUsers, user ); Arrays.remove( this._pendingUsers, user );
this._signUpTokens.delete( userID );
this._ums.tokenDB._tokens.delete( token.id );
this.users.push( user ); this.users.push( user );
await this.save(); await this.save();
@ -107,6 +119,37 @@ export class UserDB
return Promise.resolve( isValid ); return Promise.resolve( isValid );
} }
async changePassword( userID:string, tokenID:string, password:string, ip:string ):Promise<boolean>
{
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<Token> async login( email:string, password:string, ip:string ):Promise<Token>
{ {
let user = await this.byEmail( email ); let user = await this.byEmail( email );

View File

@ -27,6 +27,8 @@ export class UserManagementServerSettings
// User settings // User settings
maxLogins:number = 5; maxLogins:number = 5;
signUpConfirmationURL:string;
changePasswordURL:string;
static makeAllPathsAbsolute( settings:UserManagementServerSettings ) static makeAllPathsAbsolute( settings:UserManagementServerSettings )

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { CryptIO } from "../../crypt/CryptIO"; import { CryptIO } from "../../../crypt/CryptIO";
import { RJLog } from "../../log/RJLog"; import { RJLog } from "../../../log/RJLog";
import { RequestHandler, RequestType } from "../RequestHandler"; import { RequestHandler, RequestType } from "../../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
export class ConfirmSignUpHandler extends RequestHandler export class ConfirmSignUpHandler extends RequestHandler

View File

@ -1,16 +1,16 @@
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity";
import { CryptIO } from "../../crypt/CryptIO"; import { CryptIO } from "../../../crypt/CryptIO";
import { RJLog } from "../../log/RJLog"; import { RJLog } from "../../../log/RJLog";
import { RequestHandler, RequestType } from "../RequestHandler"; import { RequestHandler, RequestType } from "../../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; 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 { ConfirmSignUpHandler } from "./confirm-signup";
import { Session } from "../Session"; import { Session } from "../../Session";
import { LocationData } from "../location/LocationData"; import { LocationData } from "../../location/LocationData";
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp"; import { ISOTimeStamp } from "../../../../browser/date/ISOTimeStamp";
import { DateFormatter } from "../../../browser/date/DateFormatter"; import { DateFormatter } from "../../../../browser/date/DateFormatter";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn"; import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn";
export class UserLoginDataInfo 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 ); return this.sendDataInfo( "User Info", info );
} }

View File

@ -1,14 +1,14 @@
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity";
import { CryptIO } from "../../crypt/CryptIO"; import { CryptIO } from "../../../crypt/CryptIO";
import { RJLog } from "../../log/RJLog"; import { RJLog } from "../../../log/RJLog";
import { RequestHandler, RequestType } from "../RequestHandler"; import { RequestHandler, RequestType } from "../../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; 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 { ConfirmSignUpHandler } from "./confirm-signup";
import { Session } from "../Session"; import { Session } from "../../Session";
import { UserLoginData } from "../UserData"; import { UserLoginData } from "../../UserData";
import { ISOTimeStamp } from "../../../browser/date/ISOTimeStamp"; import { ISOTimeStamp } from "../../../../browser/date/ISOTimeStamp";
import { Arrays } from "../../../browser/tools/Arrays"; import { Arrays } from "../../../../browser/tools/Arrays";
export class LoginHandler extends RequestHandler export class LoginHandler extends RequestHandler
{ {

View File

@ -1,12 +1,12 @@
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity";
import { CryptIO } from "../../crypt/CryptIO"; import { CryptIO } from "../../../crypt/CryptIO";
import { RJLog } from "../../log/RJLog"; import { RJLog } from "../../../log/RJLog";
import { RequestHandler, RequestType } from "../RequestHandler"; import { RequestHandler, RequestType } from "../../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; 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 { ConfirmSignUpHandler } from "./confirm-signup";
import { Session } from "../Session"; import { Session } from "../../Session";
import { UserIsLoggedIn } from "../requirements/user/UserIsLoggedIn"; import { UserIsLoggedIn } from "../../requirements/user/UserIsLoggedIn";
export class LogoutHandler extends RequestHandler export class LogoutHandler extends RequestHandler
{ {

View File

@ -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 =
`
<b>Hi {{${RequestPasswordChangeHandler.USER_NAME}}}!</b>
<br>
You requested to change your password, here is your link:
<br>
<a href="{{${RequestPasswordChangeHandler.CHANGE_PASSWORD_LINK}}}">CHANGE PASSWORD</a>
<br>
<br>
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<void>
{
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();
}
}

View File

@ -1,9 +1,9 @@
import { RegExpUtility } from "../../../browser/text/RegExpUtitlity"; import { RegExpUtility } from "../../../../browser/text/RegExpUtitlity";
import { CryptIO } from "../../crypt/CryptIO"; import { CryptIO } from "../../../crypt/CryptIO";
import { RJLog } from "../../log/RJLog"; import { RJLog } from "../../../log/RJLog";
import { RequestHandler, RequestType } from "../RequestHandler"; import { RequestHandler, RequestType } from "../../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; 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 { ConfirmSignUpHandler } from "./confirm-signup";
export class SignUpHandler extends RequestHandler export class SignUpHandler extends RequestHandler
@ -55,19 +55,19 @@ export class SignUpHandler extends RequestHandler
let token = await this.userDB.createSignUpConfirmation( userData, this.ip ); 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 id = userData.id;
let link = `${confirmationURL}?id=${id}&token=${token.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" ); return this.sendInfo( "User created, email sent" );
} }
async sendEmail( email:string, userName:string, link:string ):Promise<void> async sendSignUpEmail( email:string, userName:string, link:string ):Promise<void>
{ {
let variables:Variables = { let variables:Variables = {

View File

@ -1,6 +1,7 @@
import { RequestHandler } from "../RequestHandler"; import { RequestHandler } from "../RequestHandler";
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
import { UserManagementServer } from "../UserManagementServer"; import { UserManagementServer } from "../UserManagementServer";
import { Message } from "../../../browser/messages/Message";
export abstract class RequestRequirement export abstract class RequestRequirement
{ {
@ -24,6 +25,6 @@ export abstract class RequestRequirement
} }
abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean> abstract handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
} }

View File

@ -5,6 +5,7 @@ import { MapList } from "../../../../browser/tools/MapList";
import { DateHelper } from "../../../../browser/date/DateHelper"; import { DateHelper } from "../../../../browser/date/DateHelper";
import { Duration } from "../../../../browser/date/Duration"; import { Duration } from "../../../../browser/date/Duration";
import { DateMath } from "../../../../browser/date/DateMath"; import { DateMath } from "../../../../browser/date/DateMath";
import { Message } from "../../../../browser/messages/Message";
export class NotTooManyRequests extends RequestRequirement export class NotTooManyRequests extends RequestRequirement
@ -17,7 +18,7 @@ export class NotTooManyRequests extends RequestRequirement
_blockList = new Map<string,number>() _blockList = new Map<string,number>()
_blockDuration = Duration.fromHours( 4 ); _blockDuration = Duration.fromHours( 4 );
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean> async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
{ {
if ( this._blockList.has( request.ip ) ) if ( this._blockList.has( request.ip ) )
{ {
@ -25,13 +26,18 @@ export class NotTooManyRequests extends RequestRequirement
if ( ! DateMath.isExpired( new Date( blockTime + this._blockDuration ) ) ) 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 ); 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<boolean> async updateIP( ip:string ):Promise<boolean>

View File

@ -1,6 +1,7 @@
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
import { RequestRequirement } from '../RequestRequirement'; import { RequestRequirement } from '../RequestRequirement';
import { Message } from '../../../../browser/messages/Message';
export class UserHasPermission extends RequestRequirement export class UserHasPermission extends RequestRequirement
{ {
@ -12,12 +13,19 @@ export class UserHasPermission extends RequestRequirement
this._permissionID = permissionID; this._permissionID = permissionID;
} }
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean> async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
{ {
let user = await this._handler.getUser(); let user = await this._handler.getUser();
let userDB = this._handler.userDB; 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( [] );
} }
} }

View File

@ -1,18 +1,19 @@
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
import { RequestRequirement } from '../RequestRequirement'; import { RequestRequirement } from '../RequestRequirement';
import { Message } from '../../../../browser/messages/Message';
export class UserIsLoggedIn extends RequestRequirement export class UserIsLoggedIn extends RequestRequirement
{ {
async handle( request:FastifyRequest, reply:FastifyReply ):Promise<boolean> async handle( request:FastifyRequest, reply:FastifyReply ):Promise<Message[]>
{ {
let requestBody = request.body; let requestBody = request.body;
let tokenData = requestBody as { token:string }; let tokenData = requestBody as { token:string };
if ( ! tokenData ) if ( ! tokenData )
{ {
return Promise.resolve( false ); return Promise.resolve( [ Message.Error( "No token data" )] );
} }
let tokenID = tokenData.token; let tokenID = tokenData.token;
@ -21,7 +22,7 @@ export class UserIsLoggedIn extends RequestRequirement
if ( ! session ) 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 ); let token = this._handler._ums.tokenDB._tokens.get( tokenID );
@ -30,9 +31,9 @@ export class UserIsLoggedIn extends RequestRequirement
if ( ! isValid ) if ( ! isValid )
{ {
return Promise.resolve( false ); return Promise.resolve( [ Message.Error( "Invalid token" + tokenID )] );
} }
return Promise.resolve( true ); return Promise.resolve( [] );
} }
} }