From 8a33c029c7493b9db2a4f3dbabf445262eedd7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Mon, 27 May 2024 21:27:36 +0200 Subject: [PATCH] [mod] notifications --- source/api/actions/member_register.ts | 9 + source/conf.ts | 20 +- source/helpers.ts | 60 ++++++ source/services/member.ts | 282 +++++++++++++++----------- 4 files changed, 252 insertions(+), 119 deletions(-) diff --git a/source/api/actions/member_register.ts b/source/api/actions/member_register.ts index f4fc4b0..7797bb4 100644 --- a/source/api/actions/member_register.ts +++ b/source/api/actions/member_register.ts @@ -29,6 +29,7 @@ namespace _espe.api email_use_nominal_address : boolean; email_redirect_to_private_address : boolean; password : (null | string); + notification_target_url_template ?: (null | string); }, Array< { @@ -67,6 +68,11 @@ namespace _espe.api "nullable": true, "description": "Passwort für alle Netz-Dienste", }, + "notification_target_url_template": { + "type": "string", + "nullable": true, + "description": "Platz-Halter: id" + }, }, "required": [ "email_use_veiled_address", @@ -128,6 +134,9 @@ namespace _espe.api "email_use_nominal_address": input.email_use_nominal_address, "email_redirect_to_private_address": input.email_redirect_to_private_address, "password": input.password, + }, + { + "notification_target_url_template": (input.notification_target_url_template ?? null), } ) .then( diff --git a/source/conf.ts b/source/conf.ts index 5098ce4..a330dd8 100644 --- a/source/conf.ts +++ b/source/conf.ts @@ -96,12 +96,21 @@ namespace _espe.conf settings : { target_domain : string; frontend_url_base : (null | string); + login_url : (null | string); prefix_for_nominal_email_addresses : string; facultative_membership_number : boolean; + summon_email : { + subject : string; + body : string; + }; registration_email : { subject : string; body : string; }; + activation_email : { + subject : string; + body : string; + }; password_policy : { minimum_length : (null | int); maximum_length : (null | int); @@ -248,12 +257,21 @@ namespace _espe.conf ((node_settings) => ({ "target_domain": (node_settings["target_domain"] ?? "example.org"), "frontend_url_base": (node_settings["frontend_url_base"] ?? null), // TODO: mandatory? + "login_url": (node_settings["login_url"] ?? null), "prefix_for_nominal_email_addresses": (node_settings["prefix_for_nominal_email_addresses"] ?? "member-"), "facultative_membership_number": (node_settings["facultative_membership_number"] ?? false), + "summon_email": { + "subject": ((node_settings["summon_email"] ?? {})["subject"] ?? "Please register"), + "body": ((node_settings["summon_email"] ?? {})["body"] ?? "URL: {{url}}"), + }, "registration_email": { - "subject": ((node_settings["registration_email"] ?? {})["subject"] ?? "Registration"), + "subject": ((node_settings["registration_email"] ?? {})["subject"] ?? "Mmeber registered"), "body": ((node_settings["registration_email"] ?? {})["body"] ?? "URL: {{url}}"), }, + "activation_email": { + "subject": ((node_settings["activation_email"] ?? {})["subject"] ?? "Account activated"), + "body": ((node_settings["activation_email"] ?? {})["body"] ?? "URL: {{url}}\n\nLogin name: {{name_login}}\n\n"), + }, "password_policy": ( ((node_settings_password_policy) => ({ "minimum_length": ( diff --git a/source/helpers.ts b/source/helpers.ts index 2280ec7..fd76e3a 100644 --- a/source/helpers.ts +++ b/source/helpers.ts @@ -268,4 +268,64 @@ namespace _espe.helpers } } + + /** + */ + export function frontend_url_check( + ) : void + { + const frontend_url_base : (null | string) = _espe.conf.get().settings.frontend_url_base; + if (frontend_url_base === null) { + throw (new Error("no frontend url base set; add in conf as 'settings.frontend_url_base'!")); + } + else { + // do nothing + } + } + + + /** + */ + export function frontend_url_get( + template : string, + arguments_ : Record + ) : (null | string) + { + const frontend_url_base : (null | string) = _espe.conf.get().settings.frontend_url_base; + if (frontend_url_base === null) { + // throw (new Error("no frontend url base set; add in conf as 'settings.frontend_url_base'!")); + return null; + } + else { + return lib_plankton.string.coin( + "{{base}}/{{rest}}", + { + "base": frontend_url_base, + "rest": lib_plankton.string.coin(template, arguments_), + } + ); + } + } + + + /** + */ + export async function notify_admins( + subject : string, + body : string + ) : Promise + { + await _espe.helpers.email_send( + ( + ( + _espe.conf.get().admins + .map(admin => admin.email_address) + .filter(x => (x !== null)) + ) as Array + ), + subject, + body + ); + } + } diff --git a/source/services/member.ts b/source/services/member.ts index 9f243c1..bfdf03f 100644 --- a/source/services/member.ts +++ b/source/services/member.ts @@ -33,7 +33,7 @@ namespace _espe.service.member /** */ - function notify_change( + function signal_change( ) : void { _hooks_change.forEach( @@ -251,6 +251,40 @@ namespace _espe.service.member ); } + + /** + */ + async function send_activation_email( + member_object : _espe.type.member_object + ) : Promise + { + if (! member_object.enabled) { + // do nothing + } + else { + if (member_object.email_address_private === null) { + // do nothing + } + else { + await _espe.helpers.email_send( + [ + member_object.email_address_private, + ], + _espe.conf.get().settings.activation_email.subject, + lib_plankton.string.coin( + _espe.conf.get().settings.activation_email.body, + { + "name_display": name_display(member_object), + "name_login": name_login(member_object), + "url": (_espe.conf.get().settings.login_url ?? "--"), + } + ) + ); + } + } + } + + /** * gibt die vollständigen Daten aller Mitglieder aus */ @@ -330,47 +364,49 @@ namespace _espe.service.member "password_change_token": null, }; const id : _espe.type.member_id = await _espe.repository.member.create(object); - notify_change(); + signal_change(); return id; } /** * sendet an ein Mitglied eine E-Mail mit Aufforderung zur Registrierung - * - * @todo conf:settings.frontend_url_base verwenden */ export async function summon( member_id : _espe.type.member_id, url_template : string ) : Promise<(null | string)> { + _espe.helpers.frontend_url_check(); const member_object : _espe.type.member_object = await get(member_id); if (member_object.email_address_private === null) { return null; } else { - const verification : string = await _espe.helpers.verification_get(member_id); - - const url : string = lib_plankton.string.coin( + const url : (null | string) = _espe.helpers.frontend_url_get( url_template, { - "verification": verification, + "verification": await _espe.helpers.verification_get(member_id), } ); - await _espe.helpers.email_send( - [ - member_object.email_address_private, - ], - _espe.conf.get().settings.registration_email.subject, - lib_plankton.string.coin( - _espe.conf.get().settings.registration_email.body, - { - "name": member_object.name_real_value, - "url": url, - } - ) - ); + if (url === null) { + // do nothing + } + else { + await _espe.helpers.email_send( + [ + member_object.email_address_private, + ], + _espe.conf.get().settings.summon_email.subject, + lib_plankton.string.coin( + _espe.conf.get().settings.summon_email.body, + { + "name": name_display(member_object), + "url": url, + } + ) + ); + } return url; } } @@ -425,13 +461,13 @@ namespace _espe.service.member password : (null | string); }, options : { - notify_admins ?: boolean; + notification_target_url_template ?: (null | string); } = {} ) : Promise;}>> { options = Object.assign( { - "notify_admins": false, + "notification_target_url_template": null, }, options ); @@ -472,25 +508,39 @@ namespace _espe.service.member member_object.password_image = await password_image(data.password); member_object.registered = true; await _espe.repository.member.update(member_id, member_object); - - notify_change(); - - if (! options.notify_admins) { - // do nothing - } - else { - await _espe.helpers.email_send( + signal_change(); + { + const url : (null | string) = ( ( - ( - _espe.conf.get().admins - .map(admin => admin.email_address) - .filter(x => (x !== null)) - ) as Array - ), - "Eingegangene Registrierung", // TODO - ("Member-ID: " + member_id.toFixed(0)) + (options.notification_target_url_template === undefined) + || + (options.notification_target_url_template === null) + ) + ? null + : _espe.helpers.frontend_url_get( + options.notification_target_url_template, + { + "id": member_id.toFixed(0), + } + ) ); + if (url === null) { + // do nothing + } + else { + /*await*/ _espe.helpers.notify_admins( + _espe.conf.get().settings.registration_email.subject, + lib_plankton.string.coin( + _espe.conf.get().settings.registration_email.body, + { + "name_display": name_display(member_object), + "url": url, + } + ) + ); + } } + /*await*/ send_activation_email(member_object); } return Promise.resolve(flaws); @@ -526,7 +576,8 @@ namespace _espe.service.member "password_change_token": member_object_old.password_change_token, }; await _espe.repository.member.update(member_id, member_object_new); - notify_change(); + signal_change(); + /*await*/ send_activation_email(member_object_new); } @@ -540,84 +591,91 @@ namespace _espe.service.member url_template : string ) : Promise { - const frontend_url_base : (null | string) = _espe.conf.get().settings.frontend_url_base; - if (frontend_url_base === null) { - return Promise.reject(new Error("no frontend url base set; add in conf as 'settings.frontend_url_base'!")); - } - else { - const now : int = lib_plankton.base.get_current_timestamp(true); - const cooldown_time : int = _espe.conf.get().settings.password_change.cooldown_time; - const member_ids : Array<_espe.type.member_id> = await ( - (await _espe.repository.member.dump()) - .filter( - member_entry => ( - member_entry.object.registered - && - member_entry.object.enabled - && + _espe.helpers.frontend_url_check(); + const now : int = lib_plankton.base.get_current_timestamp(true); + const cooldown_time : int = _espe.conf.get().settings.password_change.cooldown_time; + const member_ids : Array<_espe.type.member_id> = await ( + (await _espe.repository.member.dump()) + .filter( + member_entry => ( + member_entry.object.registered + && + member_entry.object.enabled + && + ( ( - ( - (! (member_entry.object.email_address_private === null)) - && - (member_entry.object.email_address_private === identifier) - ) - || - (name_login(member_entry.object) === identifier) + (! (member_entry.object.email_address_private === null)) + && + (member_entry.object.email_address_private === identifier) ) + || + (name_login(member_entry.object) === identifier) ) ) - .map( - member_entry => member_entry.id - ) - ); - for await (const member_id of member_ids) { - const member_object_old : _espe.type.member_object = await _espe.repository.member.read(member_id); - if ( - (! (member_object_old.password_change_last_attempt === null)) - && - ((now - member_object_old.password_change_last_attempt) <= cooldown_time) - ) { + ) + .map( + member_entry => member_entry.id + ) + ); + for await (const member_id of member_ids) { + const member_object_old : _espe.type.member_object = await _espe.repository.member.read(member_id); + if ( + (! (member_object_old.password_change_last_attempt === null)) + && + ((now - member_object_old.password_change_last_attempt) <= cooldown_time) + ) { + lib_plankton.log.notice( + "member_password_change_cooldown_not_over", + { + "member_id": member_id, + "last_attempt": member_object_old.password_change_last_attempt, + "now": now, + } + ); + // do nothing + } + else { + if (member_object_old.email_address_private === null) { lib_plankton.log.notice( - "member_password_change_cooldown_not_over", + "member_password_change_impossible_due_to_missing_private_email_address", { "member_id": member_id, - "last_attempt": member_object_old.password_change_last_attempt, - "now": now, } ); // do nothing } else { - if (member_object_old.email_address_private === null) { - lib_plankton.log.notice( - "member_password_change_impossible_due_to_missing_private_email_address", - { - "member_id": member_id, - } - ); + // keine echte Verifizierung, der Algorithmus ist aber der passende + const token : string = await _espe.helpers.verification_get(Math.floor(Math.random() * (1 << 24))); + const member_object_new : _espe.type.member_object = { + "membership_number": member_object_old.membership_number, + "name_real_value": member_object_old.name_real_value, + "name_real_index": member_object_old.name_real_index, + "email_address_private": member_object_old.email_address_private, + "registered": member_object_old.registered, + "enabled": member_object_old.enabled, + "email_use_veiled_address": member_object_old.email_use_veiled_address, + "email_use_nominal_address": member_object_old.email_use_nominal_address, + "email_redirect_to_private_address": member_object_old.email_redirect_to_private_address, + "email_allow_sending": member_object_old.email_allow_sending, + "password_image": member_object_old.password_image, + "password_change_last_attempt": now, + "password_change_token": token, + }; + await _espe.repository.member.update(member_id, member_object_new); + // signal_change(); + // do NOT wait in order to reduce information for potential attackers + const url : (null | string) = _espe.helpers.frontend_url_get( + url_template, + { + "id": member_id.toFixed(0), + "token": token, + } + ); + if (url === null) { // do nothing } else { - // keine echte Verifizierung, der Algorithmus ist aber der passende - const token : string = await _espe.helpers.verification_get(Math.floor(Math.random() * (1 << 24))); - const member_object_new : _espe.type.member_object = { - "membership_number": member_object_old.membership_number, - "name_real_value": member_object_old.name_real_value, - "name_real_index": member_object_old.name_real_index, - "email_address_private": member_object_old.email_address_private, - "registered": member_object_old.registered, - "enabled": member_object_old.enabled, - "email_use_veiled_address": member_object_old.email_use_veiled_address, - "email_use_nominal_address": member_object_old.email_use_nominal_address, - "email_redirect_to_private_address": member_object_old.email_redirect_to_private_address, - "email_allow_sending": member_object_old.email_allow_sending, - "password_image": member_object_old.password_image, - "password_change_last_attempt": now, - "password_change_token": token, - }; - await _espe.repository.member.update(member_id, member_object_new); - // notify_change(); - // do NOT wait in order to reduce information for potential attackers /*await*/ _espe.helpers.email_send( [ member_object_old.email_address_private, @@ -627,19 +685,7 @@ namespace _espe.service.member _espe.conf.get().settings.password_change.initialization_email.body, { "name": name_display(member_object_old), - "url": lib_plankton.string.coin( - "{{base}}{{rest}}", - { - "base": frontend_url_base, - "rest": lib_plankton.string.coin( - url_template, - { - "id": member_id.toFixed(0), - "token": token, - } - ), - } - ), + "url": url, } ) ); @@ -706,7 +752,7 @@ namespace _espe.service.member "password_change_token": null, }; await _espe.repository.member.update(member_id, member_object_new); - notify_change(); + signal_change(); await _espe.helpers.email_send( [ member_object_old.email_address_private, @@ -732,7 +778,7 @@ namespace _espe.service.member ) : Promise { await _espe.repository.member.delete(id); - notify_change(); + signal_change(); } */