[mod] notifications

This commit is contained in:
roydfalk 2024-05-27 21:27:36 +02:00
parent 1483bf95a8
commit 8a33c029c7
4 changed files with 252 additions and 119 deletions

View file

@ -29,6 +29,7 @@ namespace _espe.api
email_use_nominal_address : boolean; email_use_nominal_address : boolean;
email_redirect_to_private_address : boolean; email_redirect_to_private_address : boolean;
password : (null | string); password : (null | string);
notification_target_url_template ?: (null | string);
}, },
Array< Array<
{ {
@ -67,6 +68,11 @@ namespace _espe.api
"nullable": true, "nullable": true,
"description": "Passwort für alle Netz-Dienste", "description": "Passwort für alle Netz-Dienste",
}, },
"notification_target_url_template": {
"type": "string",
"nullable": true,
"description": "Platz-Halter: id"
},
}, },
"required": [ "required": [
"email_use_veiled_address", "email_use_veiled_address",
@ -128,6 +134,9 @@ namespace _espe.api
"email_use_nominal_address": input.email_use_nominal_address, "email_use_nominal_address": input.email_use_nominal_address,
"email_redirect_to_private_address": input.email_redirect_to_private_address, "email_redirect_to_private_address": input.email_redirect_to_private_address,
"password": input.password, "password": input.password,
},
{
"notification_target_url_template": (input.notification_target_url_template ?? null),
} }
) )
.then( .then(

View file

@ -96,12 +96,21 @@ namespace _espe.conf
settings : { settings : {
target_domain : string; target_domain : string;
frontend_url_base : (null | string); frontend_url_base : (null | string);
login_url : (null | string);
prefix_for_nominal_email_addresses : string; prefix_for_nominal_email_addresses : string;
facultative_membership_number : boolean; facultative_membership_number : boolean;
summon_email : {
subject : string;
body : string;
};
registration_email : { registration_email : {
subject : string; subject : string;
body : string; body : string;
}; };
activation_email : {
subject : string;
body : string;
};
password_policy : { password_policy : {
minimum_length : (null | int); minimum_length : (null | int);
maximum_length : (null | int); maximum_length : (null | int);
@ -248,12 +257,21 @@ namespace _espe.conf
((node_settings) => ({ ((node_settings) => ({
"target_domain": (node_settings["target_domain"] ?? "example.org"), "target_domain": (node_settings["target_domain"] ?? "example.org"),
"frontend_url_base": (node_settings["frontend_url_base"] ?? null), // TODO: mandatory? "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-"), "prefix_for_nominal_email_addresses": (node_settings["prefix_for_nominal_email_addresses"] ?? "member-"),
"facultative_membership_number": (node_settings["facultative_membership_number"] ?? false), "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": { "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}}"), "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": ( "password_policy": (
((node_settings_password_policy) => ({ ((node_settings_password_policy) => ({
"minimum_length": ( "minimum_length": (

View file

@ -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<string, string>
) : (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<void>
{
await _espe.helpers.email_send(
(
(
_espe.conf.get().admins
.map(admin => admin.email_address)
.filter(x => (x !== null))
) as Array<string>
),
subject,
body
);
}
} }

View file

@ -33,7 +33,7 @@ namespace _espe.service.member
/** /**
*/ */
function notify_change( function signal_change(
) : void ) : void
{ {
_hooks_change.forEach( _hooks_change.forEach(
@ -251,6 +251,40 @@ namespace _espe.service.member
); );
} }
/**
*/
async function send_activation_email(
member_object : _espe.type.member_object
) : Promise<void>
{
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 * gibt die vollständigen Daten aller Mitglieder aus
*/ */
@ -330,47 +364,49 @@ namespace _espe.service.member
"password_change_token": null, "password_change_token": null,
}; };
const id : _espe.type.member_id = await _espe.repository.member.create(object); const id : _espe.type.member_id = await _espe.repository.member.create(object);
notify_change(); signal_change();
return id; return id;
} }
/** /**
* sendet an ein Mitglied eine E-Mail mit Aufforderung zur Registrierung * sendet an ein Mitglied eine E-Mail mit Aufforderung zur Registrierung
*
* @todo conf:settings.frontend_url_base verwenden
*/ */
export async function summon( export async function summon(
member_id : _espe.type.member_id, member_id : _espe.type.member_id,
url_template : string url_template : string
) : Promise<(null | string)> ) : Promise<(null | string)>
{ {
_espe.helpers.frontend_url_check();
const member_object : _espe.type.member_object = await get(member_id); const member_object : _espe.type.member_object = await get(member_id);
if (member_object.email_address_private === null) { if (member_object.email_address_private === null) {
return null; return null;
} }
else { else {
const verification : string = await _espe.helpers.verification_get(member_id); const url : (null | string) = _espe.helpers.frontend_url_get(
const url : string = lib_plankton.string.coin(
url_template, url_template,
{ {
"verification": verification, "verification": await _espe.helpers.verification_get(member_id),
} }
); );
await _espe.helpers.email_send( if (url === null) {
[ // do nothing
member_object.email_address_private, }
], else {
_espe.conf.get().settings.registration_email.subject, await _espe.helpers.email_send(
lib_plankton.string.coin( [
_espe.conf.get().settings.registration_email.body, member_object.email_address_private,
{ ],
"name": member_object.name_real_value, _espe.conf.get().settings.summon_email.subject,
"url": url, lib_plankton.string.coin(
} _espe.conf.get().settings.summon_email.body,
) {
); "name": name_display(member_object),
"url": url,
}
)
);
}
return url; return url;
} }
} }
@ -425,13 +461,13 @@ namespace _espe.service.member
password : (null | string); password : (null | string);
}, },
options : { options : {
notify_admins ?: boolean; notification_target_url_template ?: (null | string);
} = {} } = {}
) : Promise<Array<{incident : string; details : Record<string, any>;}>> ) : Promise<Array<{incident : string; details : Record<string, any>;}>>
{ {
options = Object.assign( options = Object.assign(
{ {
"notify_admins": false, "notification_target_url_template": null,
}, },
options options
); );
@ -472,25 +508,39 @@ namespace _espe.service.member
member_object.password_image = await password_image(data.password); member_object.password_image = await password_image(data.password);
member_object.registered = true; member_object.registered = true;
await _espe.repository.member.update(member_id, member_object); await _espe.repository.member.update(member_id, member_object);
signal_change();
notify_change(); {
const url : (null | string) = (
if (! options.notify_admins) {
// do nothing
}
else {
await _espe.helpers.email_send(
( (
( (options.notification_target_url_template === undefined)
_espe.conf.get().admins ||
.map(admin => admin.email_address) (options.notification_target_url_template === null)
.filter(x => (x !== null)) )
) as Array<string> ? null
), : _espe.helpers.frontend_url_get(
"Eingegangene Registrierung", // TODO options.notification_target_url_template,
("Member-ID: " + member_id.toFixed(0)) {
"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); return Promise.resolve(flaws);
@ -526,7 +576,8 @@ namespace _espe.service.member
"password_change_token": member_object_old.password_change_token, "password_change_token": member_object_old.password_change_token,
}; };
await _espe.repository.member.update(member_id, member_object_new); 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 url_template : string
) : Promise<void> ) : Promise<void>
{ {
const frontend_url_base : (null | string) = _espe.conf.get().settings.frontend_url_base; _espe.helpers.frontend_url_check();
if (frontend_url_base === null) { const now : int = lib_plankton.base.get_current_timestamp(true);
return Promise.reject<void>(new Error("no frontend url base set; add in conf as 'settings.frontend_url_base'!")); const cooldown_time : int = _espe.conf.get().settings.password_change.cooldown_time;
} const member_ids : Array<_espe.type.member_id> = await (
else { (await _espe.repository.member.dump())
const now : int = lib_plankton.base.get_current_timestamp(true); .filter(
const cooldown_time : int = _espe.conf.get().settings.password_change.cooldown_time; member_entry => (
const member_ids : Array<_espe.type.member_id> = await ( member_entry.object.registered
(await _espe.repository.member.dump()) &&
.filter( member_entry.object.enabled
member_entry => ( &&
member_entry.object.registered (
&&
member_entry.object.enabled
&&
( (
( (! (member_entry.object.email_address_private === null))
(! (member_entry.object.email_address_private === null)) &&
&& (member_entry.object.email_address_private === identifier)
(member_entry.object.email_address_private === identifier)
)
||
(name_login(member_entry.object) === identifier)
) )
||
(name_login(member_entry.object) === identifier)
) )
) )
.map( )
member_entry => member_entry.id .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); for await (const member_id of member_ids) {
if ( const member_object_old : _espe.type.member_object = await _espe.repository.member.read(member_id);
(! (member_object_old.password_change_last_attempt === null)) if (
&& (! (member_object_old.password_change_last_attempt === null))
((now - member_object_old.password_change_last_attempt) <= cooldown_time) &&
) { ((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( lib_plankton.log.notice(
"member_password_change_cooldown_not_over", "member_password_change_impossible_due_to_missing_private_email_address",
{ {
"member_id": member_id, "member_id": member_id,
"last_attempt": member_object_old.password_change_last_attempt,
"now": now,
} }
); );
// do nothing // do nothing
} }
else { else {
if (member_object_old.email_address_private === null) { // keine echte Verifizierung, der Algorithmus ist aber der passende
lib_plankton.log.notice( const token : string = await _espe.helpers.verification_get(Math.floor(Math.random() * (1 << 24)));
"member_password_change_impossible_due_to_missing_private_email_address", const member_object_new : _espe.type.member_object = {
{ "membership_number": member_object_old.membership_number,
"member_id": member_id, "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 // do nothing
} }
else { 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( /*await*/ _espe.helpers.email_send(
[ [
member_object_old.email_address_private, member_object_old.email_address_private,
@ -627,19 +685,7 @@ namespace _espe.service.member
_espe.conf.get().settings.password_change.initialization_email.body, _espe.conf.get().settings.password_change.initialization_email.body,
{ {
"name": name_display(member_object_old), "name": name_display(member_object_old),
"url": lib_plankton.string.coin( "url": url,
"{{base}}{{rest}}",
{
"base": frontend_url_base,
"rest": lib_plankton.string.coin(
url_template,
{
"id": member_id.toFixed(0),
"token": token,
}
),
}
),
} }
) )
); );
@ -706,7 +752,7 @@ namespace _espe.service.member
"password_change_token": null, "password_change_token": null,
}; };
await _espe.repository.member.update(member_id, member_object_new); await _espe.repository.member.update(member_id, member_object_new);
notify_change(); signal_change();
await _espe.helpers.email_send( await _espe.helpers.email_send(
[ [
member_object_old.email_address_private, member_object_old.email_address_private,
@ -732,7 +778,7 @@ namespace _espe.service.member
) : Promise<void> ) : Promise<void>
{ {
await _espe.repository.member.delete(id); await _espe.repository.member.delete(id);
notify_change(); signal_change();
} }
*/ */