[add] Funktionalität zur Passwort-Änderung
This commit is contained in:
parent
08442edb12
commit
aeda24c6fb
9 changed files with 355 additions and 7 deletions
63
source/api/actions/member_password_change_execute.ts
Normal file
63
source/api/actions/member_password_change_execute.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
namespace _espe.api
|
||||
{
|
||||
|
||||
/**
|
||||
* @todo ausgeklügelte Durchsatzratenbegrenzung
|
||||
*/
|
||||
export function register_member_password_change_execute(
|
||||
rest_subject : lib_plankton.rest.type_rest
|
||||
) : void
|
||||
{
|
||||
register<
|
||||
{
|
||||
token : string;
|
||||
password_new : string;
|
||||
},
|
||||
null
|
||||
>(
|
||||
rest_subject,
|
||||
lib_plankton.http.enum_method.patch,
|
||||
"/member/password_change/execute/:id",
|
||||
{
|
||||
"description": "Führt eine Passwort-Änderung für ein Mitglied durch",
|
||||
"input_schema": () => ({
|
||||
"nullable": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"nullable": false,
|
||||
"type": "string"
|
||||
},
|
||||
"password_new": {
|
||||
"nullable": false,
|
||||
"type": "string",
|
||||
"description": "das neue Passwort"
|
||||
},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"token",
|
||||
"password_new",
|
||||
]
|
||||
}),
|
||||
"output_schema": () => ({
|
||||
"nullable": true
|
||||
}),
|
||||
"restriction": restriction_none,
|
||||
"execution": async ({"path_parameters": path_parameters, "input": input}) => {
|
||||
const member_id : _espe.type.member_id = parseInt(path_parameters["id"]);
|
||||
await _espe.service.member.password_change_execute(
|
||||
member_id,
|
||||
input.token,
|
||||
input.password_new
|
||||
);
|
||||
return Promise.resolve({
|
||||
"status_code": 200,
|
||||
"data": null
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
60
source/api/actions/member_password_change_initialize.ts
Normal file
60
source/api/actions/member_password_change_initialize.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
namespace _espe.api
|
||||
{
|
||||
|
||||
/**
|
||||
* @todo ausgeklügelte Durchsatzratenbegrenzung
|
||||
* @todo captcha
|
||||
*/
|
||||
export function register_member_password_change_initialize(
|
||||
rest_subject : lib_plankton.rest.type_rest
|
||||
) : void
|
||||
{
|
||||
register<
|
||||
{
|
||||
identifier : string;
|
||||
url_template : string;
|
||||
},
|
||||
null
|
||||
>(
|
||||
rest_subject,
|
||||
lib_plankton.http.enum_method.post,
|
||||
"/member/password_change/initialize",
|
||||
{
|
||||
"description": "Versucht dem gegebenen Identifikator ein Mitglied zuzuordnen und sendet dem ermittelten Mitglied einen Passwort-Änderungs-Verweis an die hinterlegte private E-Mail-Adresse",
|
||||
"input_schema": () => ({
|
||||
"nullable": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"nullable": false,
|
||||
"type": "string",
|
||||
"description": "Anmelde-Name oder persönliche E-Mail-Adresse des Mitglieds"
|
||||
},
|
||||
"url_template": {
|
||||
"nullable": false,
|
||||
"type": "string",
|
||||
"description": "Schablone für URL; Platz-Halter: id,token"
|
||||
},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"identifier",
|
||||
"url_template",
|
||||
]
|
||||
}),
|
||||
"output_schema": () => ({
|
||||
"nullable": true
|
||||
}),
|
||||
"restriction": restriction_none,
|
||||
"execution": async ({"input": input}) => {
|
||||
await _espe.service.member.password_change_initialize(input.identifier, input.url_template);
|
||||
return Promise.resolve({
|
||||
"status_code": 200,
|
||||
"data": null
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,11 @@ namespace _espe.api
|
|||
_espe.api.register_member_read(rest_subject);
|
||||
_espe.api.register_member_modify(rest_subject);
|
||||
// _espe.api.register_member_delete(rest_subject);
|
||||
// password_change
|
||||
{
|
||||
_espe.api.register_member_password_change_initialize(rest_subject);
|
||||
_espe.api.register_member_password_change_execute(rest_subject);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ namespace _espe.conf
|
|||
};
|
||||
settings : {
|
||||
target_domain : string;
|
||||
frontend_url_base : (null | string);
|
||||
prefix_for_numberbased_email_addresses : string;
|
||||
registration_email : {
|
||||
subject : string;
|
||||
|
@ -102,6 +103,17 @@ namespace _espe.conf
|
|||
must_contain_number : boolean;
|
||||
must_contain_special_character : boolean;
|
||||
};
|
||||
password_change : {
|
||||
cooldown_time : int;
|
||||
initialization_email : {
|
||||
subject : string;
|
||||
body : string;
|
||||
};
|
||||
execution_email : {
|
||||
subject : string;
|
||||
body : string;
|
||||
};
|
||||
};
|
||||
name_index : {
|
||||
veil : boolean;
|
||||
salt : (null | string);
|
||||
|
@ -229,6 +241,7 @@ namespace _espe.conf
|
|||
"settings": (
|
||||
((node_settings) => ({
|
||||
"target_domain": (node_settings["target_domain"] ?? "example.org"),
|
||||
"frontend_url_base": (node_settings["frontend_url_base"] ?? null), // TODO: mandatory?
|
||||
"prefix_for_numberbased_email_addresses": (node_settings["prefix_for_numberbased_email_addresses"] ?? "member-"),
|
||||
"registration_email": {
|
||||
"subject": ((node_settings["registration_email"] ?? {})["subject"] ?? "Registration"),
|
||||
|
@ -251,6 +264,19 @@ namespace _espe.conf
|
|||
"must_contain_special_character": (node_settings_password_policy["must_contain_special_character"] ?? true),
|
||||
})) (node_settings["password_policy"] ?? {})
|
||||
),
|
||||
"password_change": (
|
||||
((node_settings_password_change) => ({
|
||||
"cooldown_time": (node_settings_password_change["cooldown_time"] ?? 86400),
|
||||
"initialization_email": {
|
||||
"subject": ((node_settings_password_change["initialization_email"] ?? {})["subject"] ?? "Password change initialized"),
|
||||
"body": ((node_settings_password_change["initialization_email"] ?? {})["body"] ?? "{{url}}"),
|
||||
},
|
||||
"execution_email": {
|
||||
"subject": ((node_settings_password_change["execution_email"] ?? {})["subject"] ?? "Password changed"),
|
||||
"body": ((node_settings_password_change["execution_email"] ?? {})["body"] ?? ""),
|
||||
},
|
||||
})) (node_settings["password_change"] ?? {})
|
||||
),
|
||||
"name_index": (
|
||||
((node_settings_password_policy) => ({
|
||||
"veil": (node_settings_password_policy["veil"] ?? false),
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace _espe.database
|
|||
/**
|
||||
*/
|
||||
const _compatible_revisions : Array<string> = [
|
||||
"r2",
|
||||
"r3",
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -61,6 +61,8 @@ namespace _espe.repository.member
|
|||
"email_redirect_to_private_address": (object.email_redirect_to_private_address ? 1 : 0),
|
||||
"email_allow_sending": (object.email_allow_sending ? 1 : 0),
|
||||
"password_image": object.password_image,
|
||||
"password_change_last_attempt": object.password_change_last_attempt,
|
||||
"password_change_token": object.password_change_token,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -83,6 +85,8 @@ namespace _espe.repository.member
|
|||
"email_redirect_to_private_address": (row["email_redirect_to_private_address"] > 0),
|
||||
"email_allow_sending": (row["email_allow_sending"] > 0),
|
||||
"password_image": row["password_image"],
|
||||
"password_change_last_attempt": row["password_change_last_attempt"],
|
||||
"password_change_token": row["password_change_token"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -207,6 +207,24 @@ namespace _espe.service.member
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* ermittelt das Passwort-Abbild anhand des tatsächlichen Passworts
|
||||
*/
|
||||
function password_image(
|
||||
password : (null | string)
|
||||
) : Promise<string>
|
||||
{
|
||||
return (
|
||||
(
|
||||
(! (password === null))
|
||||
&&
|
||||
(! (password === ""))
|
||||
)
|
||||
? _espe.helpers.bcrypt_compute(password)
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* gibt die vollständigen Daten aller Mitglieder aus
|
||||
*/
|
||||
|
@ -282,6 +300,8 @@ namespace _espe.service.member
|
|||
"email_redirect_to_private_address": false,
|
||||
"email_allow_sending": false,
|
||||
"password_image": null,
|
||||
"password_change_last_attempt": null,
|
||||
"password_change_token": null,
|
||||
};
|
||||
const id : _espe.type.member_id = await _espe.repository.member.create(object);
|
||||
notify_change();
|
||||
|
@ -291,6 +311,8 @@ namespace _espe.service.member
|
|||
|
||||
/**
|
||||
* 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,
|
||||
|
@ -412,11 +434,7 @@ namespace _espe.service.member
|
|||
member_object.email_use_veiled_address = data.email_use_veiled_address
|
||||
member_object.email_use_nominal_address = data.email_use_nominal_address
|
||||
member_object.email_redirect_to_private_address = data.email_redirect_to_private_address;
|
||||
member_object.password_image = (
|
||||
password_set
|
||||
? await _espe.helpers.bcrypt_compute(data.password)
|
||||
: null
|
||||
);
|
||||
member_object.password_image = await password_image(data.password);
|
||||
member_object.registered = true;
|
||||
await _espe.repository.member.update(member_id, member_object);
|
||||
|
||||
|
@ -432,7 +450,7 @@ namespace _espe.service.member
|
|||
.map(admin => admin.email_address)
|
||||
.filter(x => (x !== null))
|
||||
),
|
||||
"Eingegangene Registrierung",
|
||||
"Eingegangene Registrierung", // TODO
|
||||
("Member-ID: " + member_id.toFixed(0))
|
||||
);
|
||||
}
|
||||
|
@ -467,12 +485,180 @@ namespace _espe.service.member
|
|||
"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": member_object_old.password_change_last_attempt,
|
||||
"password_change_token": member_object_old.password_change_token,
|
||||
};
|
||||
await _espe.repository.member.update(member_id, member_object_new);
|
||||
notify_change();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* bereitet eine Passwort-Rücksetzung für Mitglieder vor
|
||||
*
|
||||
* @todo Zwangs-Pause falls Abklingzeit noch nicht vorbei um vorzugaukeln, dass es geklappt hat?
|
||||
*/
|
||||
export async function password_change_initialize(
|
||||
identifier : string,
|
||||
url_template : string
|
||||
) : Promise<void>
|
||||
{
|
||||
if (_espe.conf.get().settings.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.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)
|
||||
) {
|
||||
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_impossible_due_to_missing_private_email_address",
|
||||
{
|
||||
"member_id": member_id,
|
||||
}
|
||||
);
|
||||
// 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();
|
||||
await _espe.helpers.email_send(
|
||||
[
|
||||
member_object_old.email_address_private,
|
||||
],
|
||||
_espe.conf.get().settings.password_change.initialization_email.subject,
|
||||
lib_plankton.string.coin(
|
||||
_espe.conf.get().settings.password_change.initialization_email.body,
|
||||
{
|
||||
"name": member_object_old.name_real_value,
|
||||
"url": lib_plankton.string.coin(
|
||||
"{{base}}{{rest}}",
|
||||
{
|
||||
"base": _espe.conf.get().settings.frontend_url_base,
|
||||
"rest": lib_plankton.string.coin(
|
||||
url_template,
|
||||
{
|
||||
"id": member_id.toFixed(0),
|
||||
"token": token,
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* führt eine Passwort-Rücksetzung für ein Mitglied durch
|
||||
*
|
||||
* @todo zeitliche Begrenzung?
|
||||
*/
|
||||
export async function password_change_execute(
|
||||
member_id : _espe.type.member_id,
|
||||
token : string,
|
||||
password_new : string
|
||||
) : Promise<void>
|
||||
{
|
||||
const member_object_old : _espe.type.member_object = await _espe.repository.member.read(member_id);
|
||||
if (! (token === member_object_old.password_change_token)) {
|
||||
lib_plankton.log.notice(
|
||||
"member_password_change_token_invalid",
|
||||
{
|
||||
"member_id": member_id,
|
||||
"token_sent": token,
|
||||
}
|
||||
);
|
||||
throw (new Error("password change token is invalid"));
|
||||
}
|
||||
else {
|
||||
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": await password_image(password_new),
|
||||
"password_change_last_attempt": member_object_old.password_change_last_attempt,
|
||||
"password_change_token": null,
|
||||
};
|
||||
await _espe.repository.member.update(member_id, member_object_new);
|
||||
notify_change();
|
||||
await _espe.helpers.email_send(
|
||||
[
|
||||
member_object_old.email_address_private,
|
||||
],
|
||||
_espe.conf.get().settings.password_change.execution_email.subject,
|
||||
lib_plankton.string.coin(
|
||||
_espe.conf.get().settings.password_change.execution_email.body,
|
||||
{
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
export async function remove(
|
||||
|
|
|
@ -20,6 +20,8 @@ namespace _espe.type
|
|||
email_redirect_to_private_address : boolean;
|
||||
email_allow_sending : boolean;
|
||||
password_image : (null | string);
|
||||
password_change_last_attempt : (null | int);
|
||||
password_change_token : (null | string);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ ${dir_temp}/espe-core.js ${dir_temp}/espe-core.d.ts: \
|
|||
${dir_source}/api/actions/member_list.ts \
|
||||
${dir_source}/api/actions/member_read.ts \
|
||||
${dir_source}/api/actions/member_modify.ts \
|
||||
${dir_source}/api/actions/member_password_change_initialize.ts \
|
||||
${dir_source}/api/actions/member_password_change_execute.ts \
|
||||
${dir_source}/api/functions.ts \
|
||||
${dir_source}/conf.ts
|
||||
@ ${cmd_log} "compile | core …"
|
||||
|
|
Loading…
Add table
Reference in a new issue