[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_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(

View file

@ -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": (

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
{
_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
*/
@ -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<Array<{incident : string; details : Record<string, any>;}>>
{
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<string>
),
"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<void>
{
const frontend_url_base : (null | string) = _espe.conf.get().settings.frontend_url_base;
if (frontend_url_base === null) {
return Promise.reject<void>(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<void>
{
await _espe.repository.member.delete(id);
notify_change();
signal_change();
}
*/