[task-207] eigentlich schon fertsch :)
This commit is contained in:
parent
8a0e1b1843
commit
3b4a7809dc
14 changed files with 395 additions and 133 deletions
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -434,7 +428,6 @@ namespace _espe.conf
|
|||
),
|
||||
})) (conf_raw["settings"] ?? {})
|
||||
),
|
||||
"admins": (conf_raw["admins"] ?? []),
|
||||
"outputs": (() => {
|
||||
switch (version) {
|
||||
case 1:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace _espe.database
|
|||
/**
|
||||
*/
|
||||
const _compatible_revisions : Array<string> = [
|
||||
"r5",
|
||||
"r6",
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
171
source/repositories/admin.ts
Normal file
171
source/repositories/admin.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
163
source/services/admin.ts
Normal 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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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}}",
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 \
|
||||
|
|
Loading…
Add table
Reference in a new issue