[task-207] eigentlich schon fertsch :)

This commit is contained in:
roydfalk 2025-03-31 18:39:50 +00:00
parent 8a0e1b1843
commit 3b4a7809dc
14 changed files with 395 additions and 133 deletions

View file

@ -56,13 +56,6 @@
"login_url": null
}
},
"admins": [
{
"name": "admin",
"password_image": "$2b$12$xOa6iWPOMjiwJ3oIOZWDGu/w2Ca/eKLHWE7aDItkNsP/79nJk065i",
"email_address": "espe-admin@example.org"
}
],
"output": {
"authelia": "/tmp/authelia-users.yml"
}

View file

@ -64,15 +64,15 @@ namespace _espe.api
return Promise.reject(new Error("impossible"));
}
else {
const admin : (null | _espe.service.admin.type_value) = await _espe.service.admin.login(input.name, input.password);
if (admin === null) {
const admin_entry : (null | _espe.service.admin.type_value) = await _espe.service.admin.login(input.name, input.password);
if (admin_entry === null) {
return Promise.resolve({
"status_code": 403,
"data": null,
});
}
else {
const session_key : string = await lib_plankton.session.begin(admin.name);
const session_key : string = await lib_plankton.session.begin(admin_entry.object.name);
return Promise.resolve({
"status_code": 201,
"data": session_key,

View file

@ -169,14 +169,6 @@ namespace _espe.conf
login_url : (null | string);
};
};
// TODO: evtl. in Datenbank verlagern
admins : Array<
{
name : string;
password_image : string;
email_address : (null | string);
}
>;
outputs : Array<
{
kind : "authelia_file";
@ -221,7 +213,7 @@ namespace _espe.conf
conf_raw : any
) : void
{
const version : int = (conf_raw["version"] ?? 4);
const version : int = (conf_raw["version"] ?? 5);
_data = {
"general": (
((node_general) => ({
@ -245,7 +237,8 @@ namespace _espe.conf
}
case 2:
case 3:
case 4: {
case 4:
case 5: {
const node_log = (
conf_raw["log"]
??
@ -286,7 +279,8 @@ namespace _espe.conf
return "::";
break;
}
case 4: {
case 4:
case 5: {
return (node_server["host"] ?? "::");
break
}
@ -398,7 +392,7 @@ namespace _espe.conf
"remark": (node_settings_summon_email["remark"] ?? null),
})) (node_settings["summon_email"] ?? {})
),
"password_policy": (
"password_policy": (
((node_settings_password_policy) => ({
"minimum_length": (
("minimum_length" in node_settings_password_policy)
@ -434,7 +428,6 @@ namespace _espe.conf
),
})) (conf_raw["settings"] ?? {})
),
"admins": (conf_raw["admins"] ?? []),
"outputs": (() => {
switch (version) {
case 1:

View file

@ -20,6 +20,7 @@
"help.args.action.options.email_test": "eine Test-E-Mail senden",
"help.args.action.options.expose_conf": "Vollständige Konfiguration ausgeben",
"help.args.action.options.password_image": "Passwort-Abbild errechnen und auf Standard-Ausgabe schreiben",
"help.args.action.options.admin_add": "Einen Administrator-Zugang anlegen",
"help.args.action.options.export_authelia": "Export der Nutzer-Datenbank im Authelia-user-Datei-Format auf Standard-Ausgabe schreiben",
"help.args.action.options.help": "Diese Hilfe auf Standard-Ausgabe schreiben",
"help.args.conf_path.description": "Pfad zur Konfigurations-Datei",

View file

@ -20,6 +20,7 @@
"help.args.action.options.email_test": "send a test e-mail",
"help.args.action.options.expose_conf": "write complete configuration to stdout",
"help.args.action.options.password_image": "compute password image and write to stdout",
"help.args.action.options.admin_add": "create an admin account",
"help.args.action.options.export_authelia": "export user database in Authelia user file format and write to stdout",
"help.args.action.options.help": "write this help to stdout",
"help.args.conf_path.description": "path to configuration file",

View file

@ -19,7 +19,7 @@ namespace _espe.database
/**
*/
const _compatible_revisions : Array<string> = [
"r5",
"r6",
];

View file

@ -248,25 +248,4 @@ namespace _espe.helpers
}
}
/**
*/
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

@ -112,6 +112,10 @@ async function main(
"name": "password-image",
"description": lib_plankton.translate.get("help.args.action.options.password_image")
},
{
"name": "admin-add",
"description": lib_plankton.translate.get("help.args.action.options.admin_add")
},
{
"name": "export-authelia",
"description": lib_plankton.translate.get("help.args.action.options.export_authelia")
@ -153,6 +157,15 @@ async function main(
"name": "arg2",
"hidden": true,
}),
"arg3": lib_plankton.args.class_argument.positional({
"index": 3,
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": null,
// "info": null,
"name": "arg3",
"hidden": true,
}),
"conf_path": lib_plankton.args.class_argument.volatile({
"indicators_long": ["conf_path"],
"indicators_short": ["c"],
@ -257,13 +270,7 @@ async function main(
(
(args["arg1"] !== null)
? [args["arg1"]]
: (
(
_espe.conf.get().admins
.map(admin => admin.email_address)
.filter(x => (x !== null))
) as Array<string>
)
: (await _espe.service.admin.email_addresses())
),
lib_plankton.string.coin(
"{{head}} | Test",
@ -275,6 +282,23 @@ async function main(
);
break;
}
case "admin-add": {
const name : (null | string) = args["arg1"];
const email_address : (null | string) = args["arg2"];
const password : (null | string) = args["arg3"];
if ((name === null) || (email_address === null) || (password === null)) {
throw (new Error("SYNTAX: admin-add <name> <email-address> <password>"));
}
else {
const admin_id : _espe.type.admin_id = await _espe.service.admin.add(
name,
email_address,
password
);
process.stdout.write(admin_id.toFixed(0) + "\n")
}
break;
}
case "serve": {
// prepare database
await _espe.database.check();

View file

@ -0,0 +1,171 @@
/*
Espe | Ein schlichtes Werkzeug zur Mitglieder-Verwaltung | Backend
Copyright (C) 2024 Christian Fraß
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see
<https://www.gnu.org/licenses/>.
*/
namespace _espe.repository.admin
{
/**
*/
var _store : (
null
|
lib_plankton.storage.type_store<
_espe.type.admin_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
function get_store(
) : lib_plankton.storage.type_store<
_espe.type.admin_id,
Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
if (_store === null) {
_store = lib_plankton.storage.sql_table_autokey_store(
{
"database_implementation": _espe.helpers.database_implementation(),
"table_name": "admins",
"key_name": "id",
}
);
}
else {
// do nothing
}
return _store;
}
/**
*/
type type_dispersal = Record<string, any>;
/**
*/
function encode(
object : _espe.type.admin_object
) : type_dispersal
{
return {
"name": object.name,
"email_address": object.email_address,
"password_image": object.password_image,
"password_fail_count": object.password_fail_count,
};
}
/**
*/
function decode(
dispersal : type_dispersal
) : _espe.type.admin_object
{
return {
"name": dispersal["name"],
"email_address": dispersal["email_address"],
"password_image": dispersal["password_image"],
"password_fail_count": dispersal["password_fail_count"],
};
}
/**
*/
export async function list(
search_term : (null | string)
) : Promise<
Array<
{
id : _espe.type.admin_id;
preview : _espe.type.admin_object;
}
>
>
{
return (
(await get_store().search(null))
.filter(
({"key": key, "preview": preview}) => (
(
(search_term === null)
||
(search_term.length <= 1)
)
?
true
:
(preview["name"].toLowerCase().includes(search_term.toLowerCase()))
)
)
.map(
({"key": key, "preview": preview}) => ({
"id": key,
"preview": decode(preview),
})
)
);
}
/**
*/
export async function read(
id : _espe.type.admin_id
) : Promise<_espe.type.admin_object>
{
const row : Record<string, any> = await get_store().read(id);
const dispersal : type_dispersal = row;
return decode(dispersal);
}
/**
*/
export function create(
value : _espe.type.admin_object
) : Promise<_espe.type.admin_id>
{
const dispersal : type_dispersal = encode(value);
return get_store().create(dispersal);
}
/**
*/
export function update(
id : _espe.type.admin_id,
value : _espe.type.admin_object
) : Promise<void>
{
const dispersal : type_dispersal = encode(value);
return get_store().update(id, dispersal);
}
}

View file

@ -1,79 +0,0 @@
/*
Espe | Ein schlichtes Werkzeug zur Mitglieder-Verwaltung | Backend
Copyright (C) 2024 Christian Fraß
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see
<https://www.gnu.org/licenses/>.
*/
namespace _espe.service.admin
{
/**
*/
export type type_value = {
name : string;
password_image : string;
};
/**
*/
function get(
name : string
) : (null | type_value)
{
const admins : Array<{name : string; password_image : string;}> = _espe.conf.get().admins;
const entry : (undefined | {name : string; password_image : string;}) = admins.find(entry_ => (entry_.name === name));
return (
(entry === undefined)
? null
: {
"name": entry.name,
"password_image": entry.password_image,
}
);
}
/**
*/
async function check_password(
value : type_value,
password_given : string
) : Promise<boolean>
{
return _espe.helpers.bcrypt_compare(value.password_image, password_given);
}
/**
*/
export async function login(
name : string,
password : string
) : Promise<(null | type_value)>
{
const value : (null | type_value) = get(name);
if (value === null) {
return null;
}
else {
if (! (await check_password(value, password))) {
return null;
}
else {
return value;
}
}
}
}

163
source/services/admin.ts Normal file
View file

@ -0,0 +1,163 @@
/*
Espe | Ein schlichtes Werkzeug zur Mitglieder-Verwaltung | Backend
Copyright (C) 2024 Christian Fraß
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see
<https://www.gnu.org/licenses/>.
*/
namespace _espe.service.admin
{
/**
*/
export type type_value = {
id : _espe.type.admin_id;
object : _espe.type.admin_object;
};
/**
*/
export async function add(
name : string,
email_address : (null | string),
password : string
) : Promise<_espe.type.admin_id>
{
const admin_object : _espe.type.admin_object = {
"name": name,
"email_address": email_address,
"password_image": (await _espe.helpers.bcrypt_compute(password)),
"password_fail_count": 0,
};
return _espe.repository.admin.create(admin_object);
}
/**
*/
async function get(
name : string
) : Promise<(null | type_value)>
{
const hits : Array<
{
id : _espe.type.admin_id;
preview : _espe.type.admin_object;
}
> = await _espe.repository.admin.list(name);
return Promise.resolve<(null | type_value)>(
(hits.length === 1)
?
{"id": hits[0].id, "object": hits[0].preview}
:
null
);
}
/**
*/
async function check_password(
value : type_value,
password_given : string
) : Promise<boolean>
{
return _espe.helpers.bcrypt_compare(value.object.password_image, password_given);
}
/**
*/
export async function login(
name : string,
password : string
) : Promise<(null | type_value)>
{
const value : (null | type_value) = await get(name);
if (value === null) {
return null;
}
else {
/**
* @todo outsource fail count threshold?
*/
const password_fail_threshold : int = 3;
if (value.object.password_fail_count >= password_fail_threshold) {
lib_plankton.log.notice(
"admin_password_fail_threshold_exceeded",
{
"id": value.id,
"name": value.object.name,
"password_fail_count": (value.object.password_fail_count + 1)
}
);
await _espe.repository.admin.update(
value.id,
{
"name": value.object.name,
"email_address": value.object.email_address,
"password_image": value.object.password_image,
"password_fail_count": (value.object.password_fail_count + 1),
}
);
return null;
}
else {
if (! (await check_password(value, password))) {
await _espe.repository.admin.update(
value.id,
{
"name": value.object.name,
"email_address": value.object.email_address,
"password_image": value.object.password_image,
"password_fail_count": (value.object.password_fail_count + 1),
}
);
return null;
}
else {
return value;
}
}
}
}
/**
*/
export async function email_addresses(
) : Promise<Array<string>>
{
return (
(
(await _espe.repository.admin.list(null))
.map(hit => hit.preview["email_address"])
.filter(x => (x !== null))
) as Array<string>
);
}
/**
*/
export async function notify_all(
subject : string,
body : string
) : Promise<void>
{
return _espe.helpers.email_send(
await email_addresses(),
subject,
body
);
}
}

View file

@ -503,7 +503,7 @@ namespace _espe.service.member
}
)
);
/*await*/ _espe.helpers.notify_admins(
/*await*/ _espe.service.admin.notify_all(
lib_plankton.string.coin(
"{{head}} | {{core}}",
{

View file

@ -16,6 +16,21 @@ You should have received a copy of the GNU General Public License along with thi
namespace _espe.type
{
/**
*/
export type admin_id = int;
/**
*/
export type admin_object = {
name : string;
email_address : (null | string);
password_image : string;
password_fail_count : int;
};
/**
*/
export type member_id = int;

View file

@ -48,11 +48,12 @@ ${dir_temp}/espe-core.js ${dir_temp}/espe-core.d.ts: \
${dir_source}/helpers/password.ts \
${dir_source}/database.ts \
${dir_source}/types.ts \
${dir_source}/repositories/admin.ts \
${dir_source}/repositories/name_index.ts \
${dir_source}/repositories/member.ts \
${dir_source}/services/name_index.ts \
${dir_source}/services/member.ts \
${dir_source}/service-admin.ts \
${dir_source}/services/admin.ts \
${dir_source}/api/base.ts \
${dir_source}/api/actions/meta_ping.ts \
${dir_source}/api/actions/meta_spec.ts \