diff --git a/misc/sampledata.json b/misc/sampledata.json
new file mode 100644
index 0000000..a591264
--- /dev/null
+++ b/misc/sampledata.json
@@ -0,0 +1,42 @@
+{
+ "admins": [
+ {
+ "name": "admin",
+ "email_address": "admin@example.org",
+ "password": "admin"
+ }
+ ],
+ "members": [
+ {
+ "membership_number": "123",
+ "name_real": "Alexandra Ahorn",
+ "email_address_private": "alex-rockt@example.org",
+ "groups": ["auto","zug","flugzeug"]
+ },
+ {
+ "membership_number": "234",
+ "name_real": "Berthold Buche",
+ "email_address_private": "bert-ohne-ernie@example.org",
+ "groups": ["fahrrad","zu_fuß","zug"]
+ },
+ {
+ "membership_number": "345",
+ "name_real": "Charlotte Castania",
+ "email_adress_private": "charly-the-unicorn@example.org",
+ "groups": ["fahrrad","auto"]
+ }
+ ],
+ "invites": [
+ {
+ "membership_number_changeable": false,
+ "membership_number_value": "456",
+ "name_changeable": true,
+ "name_value": "Daniel Distel",
+ "email_address_changeable": true,
+ "email_address_value": "duesentrieb@example.org",
+ "groups_changeable": false,
+ "groups_value": ["flugzeug","zu_fuß"]
+ }
+ ]
+}
+
diff --git a/source/api/actions/invite_accept.ts b/source/api/actions/invite_accept.ts
new file mode 100644
index 0000000..f15356f
--- /dev/null
+++ b/source/api/actions/invite_accept.ts
@@ -0,0 +1,88 @@
+/*
+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
+.
+ */
+
+namespace _espe.api
+{
+ /**
+ */
+ export function register_invite_accept(
+ rest_subject : lib_plankton.rest_http.type_rest
+ ) : void
+ {
+ lib_plankton.rest_http.register<
+ {
+ key : string;
+ membership_number_value : (null | string);
+ name_value : string;
+ email_address_value : (null | string);
+ groups_value : Array;
+ },
+ null
+ >(
+ rest_subject,
+ lib_plankton.http.enum_method.post,
+ _espe.api.full_path("/invite/accept"),
+ {
+ /**
+ * @todo translation
+ */
+ "description": () => "nimmt eine Einladung an",
+ /**
+ * @todo
+ */
+ "input_schema": () => ({
+ "nullable": true,
+ }),
+ /**
+ * @todo
+ */
+ "output_schema": () => ({
+ "nullable": true,
+ }),
+ "restriction": () => restriction_none,
+ "execution": () => async ({"input": input}) => {
+ if (input === null) {
+ return Promise.resolve({
+ "status_code": 400,
+ "data": null
+ });
+ }
+ else {
+ try {
+ await _espe.service.invite.accept(
+ input.key,
+ input.membership_number_value,
+ input.name_value,
+ input.email_address_value,
+ input.groups_value
+ );
+ return Promise.resolve({
+ "status_code": 200,
+ "data": null
+ });
+ }
+ catch (error) {
+ return Promise.resolve({
+ "status_code": 404,
+ "data": null
+ });
+ }
+ }
+ }
+ }
+ );
+ }
+
+}
diff --git a/source/api/actions/invite_create.ts b/source/api/actions/invite_create.ts
new file mode 100644
index 0000000..f43f953
--- /dev/null
+++ b/source/api/actions/invite_create.ts
@@ -0,0 +1,196 @@
+/*
+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
+.
+ */
+
+namespace _espe.api
+{
+
+ /**
+ */
+ export function register_invite_create(
+ rest_subject : lib_plankton.rest_http.type_rest
+ ) : void
+ {
+ lib_plankton.rest_http.register<
+ {
+ membership_number_changeable : boolean;
+ membership_number_value : (null | string);
+ name_changeable : boolean;
+ name_value : string;
+ email_address_changeable : boolean;
+ email_address_value : (null | string);
+ groups_changeable : boolean;
+ groups_value : Array;
+ expiry ?: (null | int);
+ // notification_target_url_template ?: (null | string);
+ },
+ (
+ string
+ |
+ {
+ id : _espe.type.member_id;
+ key : string;
+ }
+ )
+ >(
+ rest_subject,
+ lib_plankton.http.enum_method.post,
+ _espe.api.full_path("/invite/create"),
+ {
+ /**
+ * @todo translation
+ */
+ "description": () => "erstellt eine Einladung und gibt die erzeugte ID und den erzeugten Schlüssel aus",
+ "input_schema": () => ({
+ "type": "object",
+ "nullable": false,
+ "additionalProperties": false,
+ "properties": {
+ "membership_number_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "Mitgliedsnummer | änderbar"
+ },
+ "membership_number_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "Mitgliedsnummer | Wert"
+ },
+ "name_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "Name | änderbar"
+ },
+ "name_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "Name | Wert"
+ },
+ "email_address_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "E-Mail-Adresse | änderbar"
+ },
+ "email_address_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "E-Mail-Adresse | Wert"
+ },
+ "groups_integer": {
+ "type": "integer",
+ "nullable": true,
+ "description": "Gruppen | Modus"
+ },
+ "groups_value": {
+ "nullable": false,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "nullable": false,
+ },
+ "description": "Gruppen | Wert"
+ },
+ "expiry": {
+ "nullable": true,
+ "type": "intiger",
+ "description": "Ablaufzeitpunkt"
+ },
+ /*
+ "notification_target_url_template": {
+ "type": "string",
+ "nullable": true,
+ "description": "Platz-Halter: id"
+ },
+ */
+ },
+ "required": [
+ "membership_number_changeable",
+ "membership_number_value",
+ "name_changeable",
+ "name_value",
+ "email_address_changeable",
+ "email_address_value",
+ "groups_changeable",
+ "groups_value",
+ "expiry",
+ ]
+ }),
+ "output_schema": () => ({
+ "type": "object",
+ "nullable": false,
+ "properties": {
+ "id": {
+ "type": "number",
+ "nullable": false,
+ },
+ "key": {
+ "type": "string",
+ "nullable": false,
+ },
+ },
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "key",
+ ]
+ }),
+ "restriction": () => restriction_logged_in,
+ "execution": () => async ({"input": input}) => {
+ if (input === null) {
+ return Promise.reject(new Error("impossible"));
+ }
+ else {
+ if (
+ (! _espe.conf.get().settings.misc.facultative_membership_number)
+ &&
+ (
+ (input.membership_number_value === null)
+ ||
+ (input.membership_number_value === "")
+ )
+ ) {
+ return Promise.resolve({
+ "status_code": 400,
+ "data": "membership number required"
+ });
+ }
+ else {
+ const invite_info : {id : _espe.type.invite_id; key : _espe.type.invite_key;} = await _espe.service.invite.create(
+ {
+ "membership_number_changeable": input.membership_number_changeable,
+ "membership_number_value": input.membership_number_value,
+ "name_changeable": input.name_changeable,
+ "name_value": input.name_value,
+ "email_address_changeable": input.email_address_changeable,
+ "email_address_value": input.email_address_value,
+ "groups_changeable": input.groups_changeable,
+ "groups_value": input.groups_value,
+ },
+ {
+ "expiry": input.expiry,
+ }
+ );
+ return Promise.resolve({
+ "status_code": 201,
+ "data": invite_info
+ });
+ }
+ }
+ }
+ }
+ );
+ }
+
+}
+
diff --git a/source/api/actions/invite_examine.ts b/source/api/actions/invite_examine.ts
new file mode 100644
index 0000000..163d4a0
--- /dev/null
+++ b/source/api/actions/invite_examine.ts
@@ -0,0 +1,143 @@
+/*
+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
+.
+ */
+
+namespace _espe.api
+{
+ /**
+ */
+ export function register_invite_examine(
+ rest_subject : lib_plankton.rest_http.type_rest
+ ) : void
+ {
+ lib_plankton.rest_http.register<
+ any,
+ any
+ >(
+ rest_subject,
+ lib_plankton.http.enum_method.get,
+ _espe.api.full_path("/invite/examine"),
+ {
+ /**
+ * @todo translation
+ */
+ "description": () => "gibt die Daten einer Einladung anhand ihres Schlüssels aus",
+ "query_parameters": () => [
+ {
+ "name": "key",
+ "required": true,
+ "description": "key",
+ }
+ ],
+ "output_schema": () => ({
+ "type": "object",
+ "nullable": false,
+ "additionalProperties": false,
+ "properties": {
+ "expiry": {
+ "nullable": true,
+ "type": "integer",
+ "description": "Ablaufzeitpunkt"
+ },
+ "membership_number_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "Mitgliedsnummer | änderbar"
+ },
+ "membership_number_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "Mitgliedsnummer | Wert"
+ },
+ "name_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "Name | änderbar"
+ },
+ "name_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "Name | Wert"
+ },
+ "email_address_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "E-Mail-Adresse | änderbar"
+ },
+ "email_address_value": {
+ "type": "string",
+ "nullable": true,
+ "description": "E-Mail-Adresse | Wert"
+ },
+ "groups_changeable": {
+ "type": "boolean",
+ "nullable": false,
+ "description": "Gruppen | änderbar"
+ },
+ "groups_value": {
+ "nullable": false,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "nullable": false,
+ },
+ "description": "Gruppen | Wert"
+ },
+ },
+ "required": [
+ "expiry",
+ "membership_number_mode",
+ "membership_number_value",
+ "name_mode",
+ "name_value",
+ "email_address_mode",
+ "email_address_value",
+ "groups_mode",
+ "groups_value",
+ ]
+ }),
+ "restriction": () => restriction_none,
+ "execution": () => ({"query_parameters": query_parameters, "input": input}) => {
+ const invite_key : _espe.type.invite_key = query_parameters["key"];
+ return (
+ _espe.service.invite.examine(invite_key)
+ .then(
+ (invite_object) => Promise.resolve({
+ "status_code": 200,
+ "data": {
+ "expiry": invite_object.expiry,
+ "membership_number_changeable": invite_object.membership_number_changeable,
+ "membership_number_value": invite_object.membership_number_value,
+ "name_changeable": invite_object.name_changeable,
+ "name_value": invite_object.name_value,
+ "email_address_changeable": invite_object.email_address_changeable,
+ "email_address_value": invite_object.email_address_value,
+ "groups_changeable": invite_object.groups_changeable,
+ "groups_value": invite_object.groups_value,
+ }
+ })
+ )
+ .catch(
+ (error) => Promise.resolve({
+ "status_code": 404,
+ "data": "not found"
+ })
+ )
+ );
+ }
+ }
+ );
+ }
+
+}
diff --git a/source/api/actions/invite_list.ts b/source/api/actions/invite_list.ts
new file mode 100644
index 0000000..e0bdfdf
--- /dev/null
+++ b/source/api/actions/invite_list.ts
@@ -0,0 +1,86 @@
+/*
+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
+.
+ */
+
+namespace _espe.api
+{
+ /**
+ */
+ export function register_invite_list(
+ rest_subject : lib_plankton.rest_http.type_rest
+ ) : void
+ {
+ lib_plankton.rest_http.register<
+ null,
+ Array<
+ {
+ id : _espe.type.invite_id;
+ key : _espe.type.invite_key;
+ expiry : (null | int);
+ name_value : string;
+ }
+ >
+ >(
+ rest_subject,
+ lib_plankton.http.enum_method.get,
+ _espe.api.full_path("/invite/list"),
+ {
+ /**
+ * @todo translation
+ */
+ "description": () => "listet alle Einladung auf",
+ "output_schema": () => ({
+ "type": "object",
+ "nullable": false,
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "nullable": false,
+ "type": "intiger",
+ "description": "ID"
+ },
+ "key": {
+ "nullable": false,
+ "type": "string",
+ "description": "Schlüssel"
+ },
+ "expiry": {
+ "nullable": true,
+ "type": "integer",
+ "description": "Ablauf"
+ },
+ "name_value": {
+ "nullable": false,
+ "type": "string",
+ "description": "Name"
+ },
+ },
+ "required": [
+ "id",
+ "key",
+ ]
+ }),
+ "restriction": () => restriction_logged_in,
+ "execution": () => async ({}) => {
+ const data = await _espe.service.invite.list();
+ return Promise.resolve({
+ "status_code": 200,
+ "data": data
+ });
+ }
+ }
+ );
+ }
+
+}
diff --git a/source/api/functions.ts b/source/api/functions.ts
index e2d8fd5..04dd3a1 100644
--- a/source/api/functions.ts
+++ b/source/api/functions.ts
@@ -61,6 +61,7 @@ namespace _espe.api
}
// invite
{
+ _espe.api.register_invite_list(rest_subject);
_espe.api.register_invite_create(rest_subject);
_espe.api.register_invite_examine(rest_subject);
_espe.api.register_invite_accept(rest_subject);
diff --git a/source/data/localization/deu.loc.json b/source/data/localization/deu.loc.json
index 45b7840..3a5bcc2 100644
--- a/source/data/localization/deu.loc.json
+++ b/source/data/localization/deu.loc.json
@@ -17,6 +17,7 @@
"help.args.action.description": "auszuführende Aktion; Auswahl",
"help.args.action.options.serve": "Server starten",
"help.args.action.options.api_doc": "API-Dokumentation gemäß OpenAPI-Spezifikation auf Standard-Ausgabe schreiben",
+ "help.args.action.options.sample": "Datenbank mit Beispiel-Daten befüllen",
"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",
diff --git a/source/data/localization/eng.loc.json b/source/data/localization/eng.loc.json
index f9e73af..b822ef3 100644
--- a/source/data/localization/eng.loc.json
+++ b/source/data/localization/eng.loc.json
@@ -17,6 +17,7 @@
"help.args.action.description": "action to executo; options",
"help.args.action.options.serve": "start server",
"help.args.action.options.api_doc": "write API documentation according to OpenAPI specification to stdout",
+ "help.args.action.options.sample": "fill database with sample data",
"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",
diff --git a/source/database.ts b/source/database.ts
index a0945a5..abe3db1 100644
--- a/source/database.ts
+++ b/source/database.ts
@@ -19,7 +19,7 @@ namespace _espe.database
/**
*/
const _compatible_revisions : Array = [
- "r6",
+ "r7",
];
diff --git a/source/helpers.ts b/source/helpers.ts
index f5d6b43..bb68605 100644
--- a/source/helpers.ts
+++ b/source/helpers.ts
@@ -16,6 +16,26 @@ You should have received a copy of the GNU General Public License along with thi
namespace _espe.helpers
{
+ /**
+ */
+ export function dbbool_encode(
+ value : boolean
+ ) : int
+ {
+ return (value ? 0 : 1);
+ }
+
+
+ /**
+ */
+ export function dbbool_decode(
+ value : int
+ ) : boolean
+ {
+ return (value > 0);
+ }
+
+
/**
*/
export type type_smtp_credentials = {
diff --git a/source/main.ts b/source/main.ts
index 05bafe4..7ac590b 100644
--- a/source/main.ts
+++ b/source/main.ts
@@ -204,6 +204,10 @@ namespace _espe
"name": "api-doc",
"description": lib_plankton.translate.get("help.args.action.options.api_doc")
},
+ {
+ "name": "sample",
+ "description": lib_plankton.translate.get("help.args.action.options.sample"),
+ },
{
"name": "email-test",
"description": lib_plankton.translate.get("help.args.action.options.email_test")
@@ -271,7 +275,7 @@ namespace _espe
"hidden": true,
}),
"conf_path": lib_plankton.args.class_argument.volatile({
- "indicators_long": ["conf_path"],
+ "indicators_long": ["conf-path"],
"indicators_short": ["c"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
@@ -360,6 +364,14 @@ namespace _espe
);
break;
}
+ case "sample": {
+ const path : string = args["arg1"];
+ if (path === null) {
+ throw (new Error("SYNTAX: sample "));
+ }
+ _espe.sample.fill_by_path(path);
+ break;
+ }
case "email-test": {
await _espe.helpers.email_send(
(
diff --git a/source/repositories/invite.ts b/source/repositories/invite.ts
new file mode 100644
index 0000000..8de1cb0
--- /dev/null
+++ b/source/repositories/invite.ts
@@ -0,0 +1,333 @@
+/*
+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
+.
+ */
+
+namespace _espe.repository.invite
+{
+
+ /**
+ */
+ type type_group_chest = lib_plankton.storage.type_chest<
+ Array,
+ Record,
+ lib_plankton.database.type_description_create_table,
+ lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
+ Record
+ >;
+
+
+ /**
+ */
+ var _core_store : (
+ null
+ |
+ lib_plankton.storage.type_store<
+ _espe.type.invite_id,
+ Record,
+ {},
+ lib_plankton.storage.type_sql_table_autokey_search_term,
+ Record
+ >
+ ) = null;
+
+
+ /**
+ */
+ var _group_chest : (
+ null
+ |
+ type_group_chest
+ ) = null;
+
+
+ /**
+ */
+ function get_core_store(
+ ) : lib_plankton.storage.type_store<
+ _espe.type.invite_id,
+ Record,
+ {},
+ lib_plankton.storage.type_sql_table_autokey_search_term,
+ Record
+ >
+ {
+ if (_core_store === null) {
+ _core_store = lib_plankton.storage.sql_table_autokey_store(
+ {
+ "database_implementation": _espe.helpers.database_implementation(),
+ "table_name": "invites",
+ "key_name": "id",
+ }
+ );
+ }
+ else {
+ // do nothing
+ }
+ return _core_store;
+ }
+
+
+ /**
+ */
+ function get_group_chest(
+ ) : type_group_chest
+ {
+ if (_group_chest === null) {
+ _group_chest = lib_plankton.storage.sql_table_common.chest(
+ {
+ "database_implementation": _espe.helpers.database_implementation(),
+ "table_name": "invite_groups",
+ "key_names": ["invite_id","group_name"],
+ }
+ );
+ }
+ else {
+ // do nothing
+ }
+ return _group_chest;
+ }
+
+
+ /**
+ */
+ type type_dispersal = {
+ core_row : Record;
+ group_rows : Array<
+ Record
+ >;
+ };
+
+
+ /**
+ */
+ function encode(
+ object : _espe.type.invite_object
+ ) : type_dispersal
+ {
+ return {
+ "core_row": {
+ "key": object.key,
+ "expiry": object.expiry,
+ "membership_number_changeable": _espe.helpers.dbbool_encode(object.membership_number_changeable),
+ "membership_number_value": object.membership_number_value,
+ "name_changeable": _espe.helpers.dbbool_encode(object.name_changeable),
+ "name_value": object.name_value,
+ "email_address_changeable": _espe.helpers.dbbool_encode(object.email_address_changeable),
+ "email_address_value": object.email_address_value,
+ "groups_changeable": _espe.helpers.dbbool_encode(object.groups_changeable),
+ },
+ "group_rows": (
+ object.groups_value
+ .map(
+ group => ({
+ "group_name": group,
+ })
+ )
+ )
+ };
+ }
+
+
+ /**
+ */
+ function decode(
+ dispersal : type_dispersal
+ ) : _espe.type.invite_object
+ {
+ return {
+ "key": dispersal.core_row["key"],
+ "expiry": dispersal.core_row["expiry"],
+ "membership_number_changeable": _espe.helpers.dbbool_decode(dispersal.core_row["membership_number_changeable"]),
+ "membership_number_value": dispersal.core_row["membership_number_value"],
+ "name_changeable": _espe.helpers.dbbool_decode(dispersal.core_row["name_changeable"]),
+ "name_value": dispersal.core_row["name_value"],
+ "email_address_changeable": _espe.helpers.dbbool_decode(dispersal.core_row["email_address_changeable"]),
+ "email_address_value": dispersal.core_row["email_address_value"],
+ "groups_changeable": _espe.helpers.dbbool_decode(dispersal.core_row["groups_changeable"]),
+ "groups_value": lib_plankton.list.sorted(
+ dispersal.group_rows.map(row => row["group_name"]),
+ {
+ "compare_element": (group1, group2) => (group1 <= group2)
+ }
+ ),
+ };
+ }
+
+
+ /**
+ * @todo optimize
+ */
+ export async function list(
+ search_term : (null | string)
+ ) : Promise<
+ Array<
+ {
+ id : _espe.type.invite_id;
+ preview : {
+ name : string;
+ };
+ }
+ >
+ >
+ {
+ return (
+ (await get_core_store().search(null))
+ .filter(
+ ({"key": key, "preview": preview}) => (
+ (search_term === null)
+ ?
+ true
+ :
+ (preview["key"] === search_term)
+ )
+ )
+ .map(
+ ({"key": key, "preview": preview}) => ({
+ "id": key,
+ "preview": {
+ "name": preview["name_value"],
+ }
+ })
+ )
+ );
+ }
+
+
+ /**
+ */
+ export async function read(
+ id : _espe.type.invite_id
+ ) : Promise<_espe.type.invite_object>
+ {
+ const core_row : Record = await get_core_store().read(id);
+ const group_hits : Array<{key : Record; preview : Record;}> = await get_group_chest().search(
+ {
+ "expression": "invite_id = $invite_id",
+ "arguments": {"invite_id": id}
+ }
+ );
+
+ const dispersal : type_dispersal = {
+ "core_row": core_row,
+ "group_rows": group_hits.map(
+ hit => ({
+ "group_name": hit.preview["group_name"]
+ })
+ ),
+ };
+
+ return decode(dispersal);
+ }
+
+
+ /**
+ */
+ export async function create(
+ value : _espe.type.invite_object
+ ) : Promise<_espe.type.invite_id>
+ {
+ const dispersal : type_dispersal = encode(value);
+
+ // core
+ const id : _espe.type.invite_id = await get_core_store().create(dispersal.core_row);
+
+ // groups
+ for await (const group_row of dispersal.group_rows) {
+ await get_group_chest().write(
+ [
+ id,
+ group_row["group_name"],
+ ],
+ {
+ "_dummy": null,
+ }
+ );
+ }
+
+ return id;
+ }
+
+
+ /**
+ */
+ export async function delete_(
+ id : _espe.type.invite_id
+ ) : Promise
+ {
+ // groups
+ const hits : Array<{key : Array; preview : Record;}> = await get_group_chest().search(
+ {
+ "expression": "invite_id = $invite_id",
+ "arguments": {"invite_id": id}
+ }
+ );
+ for (const hit of hits) {
+ await get_group_chest().delete(hit.key);
+ }
+
+ // core
+ await get_core_store().delete(id);
+ }
+
+
+ /**
+ * @todo optimize
+ */
+ export async function identify(
+ key : _espe.type.invite_key
+ ) : Promise<_espe.type.invite_id>
+ {
+ const hits : Array<{id : _espe.type.invite_id; preview : any;}> = await list(key);
+ return (
+ (hits.length !== 1)
+ ?
+ Promise.reject<_espe.type.invite_id>(new Error("not found"))
+ :
+ Promise.resolve<_espe.type.invite_id>(hits[0].id)
+ );
+ }
+
+
+ /**
+ */
+ export async function dump(
+ ) : Promise<
+ Array<
+ {
+ id : _espe.type.invite_id;
+ object : _espe.type.invite_object;
+ }
+ >
+ >
+ {
+ return (
+ Promise.all(
+ (await get_core_store().search(null))
+ .map(hit => hit.key)
+ .map(
+ id => (
+ read(id)
+ .then(
+ (object) => ({
+ "id": id,
+ "object": object
+ })
+ )
+ )
+ )
+ )
+ );
+ }
+
+}
diff --git a/source/repositories/member.ts b/source/repositories/member.ts
index e9e7398..cf624d4 100644
--- a/source/repositories/member.ts
+++ b/source/repositories/member.ts
@@ -121,12 +121,12 @@ namespace _espe.repository.member
"name_real_value": object.name_real_value,
"name_real_index": object.name_real_index,
"email_address_private": object.email_address_private,
- "registered": (object.registered ? 1 : 0),
- "enabled": (object.enabled ? 1 : 0),
- "email_use_veiled_address": (object.email_use_veiled_address ? 1 : 0),
- "email_use_nominal_address": (object.email_use_nominal_address ? 1 : 0),
- "email_redirect_to_private_address": (object.email_redirect_to_private_address ? 1 : 0),
- "email_allow_sending": (object.email_allow_sending ? 1 : 0),
+ "registered": _espe.helpers.dbbool_encode(object.registered),
+ "enabled": _espe.helpers.dbbool_encode(object.enabled),
+ "email_use_veiled_address": _espe.helpers.dbbool_encode(object.email_use_veiled_address),
+ "email_use_nominal_address": _espe.helpers.dbbool_encode(object.email_use_nominal_address),
+ "email_redirect_to_private_address": _espe.helpers.dbbool_encode(object.email_redirect_to_private_address),
+ "email_allow_sending": _espe.helpers.dbbool_encode(object.email_allow_sending),
"password_image": object.password_image,
"password_change_last_attempt": object.password_change_last_attempt,
"password_change_token": object.password_change_token,
@@ -160,12 +160,12 @@ namespace _espe.repository.member
"compare_element": (group1, group2) => (group1 <= group2)
}
),
- "registered": (dispersal.core_row["registered"] > 0),
- "enabled": (dispersal.core_row["enabled"] > 0),
- "email_use_veiled_address": (dispersal.core_row["email_use_veiled_address"] > 0),
- "email_use_nominal_address": (dispersal.core_row["email_use_nominal_address"] > 0),
- "email_redirect_to_private_address": (dispersal.core_row["email_redirect_to_private_address"] > 0),
- "email_allow_sending": (dispersal.core_row["email_allow_sending"] > 0),
+ "registered": _espe.helpers.dbbool_decode(dispersal.core_row["registered"]),
+ "enabled": _espe.helpers.dbbool_decode(dispersal.core_row["enabled"]),
+ "email_use_veiled_address": _espe.helpers.dbbool_decode(dispersal.core_row["email_use_veiled_address"]),
+ "email_use_nominal_address": _espe.helpers.dbbool_decode(dispersal.core_row["email_use_nominal_address"]),
+ "email_redirect_to_private_address": _espe.helpers.dbbool_decode(dispersal.core_row["email_redirect_to_private_address"]),
+ "email_allow_sending": _espe.helpers.dbbool_decode(dispersal.core_row["email_allow_sending"]),
"password_image": dispersal.core_row["password_image"],
"password_change_last_attempt": dispersal.core_row["password_change_last_attempt"],
"password_change_token": dispersal.core_row["password_change_token"],
diff --git a/source/sample.ts b/source/sample.ts
new file mode 100644
index 0000000..d435d6b
--- /dev/null
+++ b/source/sample.ts
@@ -0,0 +1,119 @@
+/*
+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
+.
+ */
+
+namespace _espe.sample
+{
+
+ /**
+ */
+ type type_data = {
+ admins : Array<
+ {
+ name : string;
+ email_address : (null | string);
+ password : string;
+ }
+ >;
+ members : Array<
+ {
+ membership_number : string;
+ name_real : string;
+ email_address_private : (null | string);
+ groups : Array;
+ }
+ >;
+ invites : Array<
+ {
+ membership_number_changeable : boolean;
+ membership_number_value : string;
+ name_changeable : boolean;
+ name_value : string;
+ email_address_changeable : boolean;
+ email_address_value : (null | string);
+ groups_changeable : boolean;
+ groups_value : Array;
+ }
+ >;
+ };
+
+
+ /**
+ */
+ export async function fill(
+ data : type_data
+ ) : Promise
+ {
+ // admins
+ {
+ for (const admin_raw of data.admins) {
+ const admin_id : _espe.type.admin_id = await _espe.service.admin.add(
+ admin_raw.name,
+ admin_raw.email_address,
+ admin_raw.password,
+ );
+ }
+ }
+ // members
+ {
+ for (const member_raw of data.members) {
+ const member_id : _espe.type.member_id = await _espe.service.member.project(
+ {
+ "membership_number": member_raw.membership_number,
+ "name_real_value": member_raw.name_real,
+ "email_address_private": member_raw.email_address_private,
+ "groups": member_raw.groups
+ },
+ {
+ "silent": true,
+ }
+ );
+ }
+ /**
+ * @todo passwords
+ */
+ }
+ // invites
+ {
+ for (const invite_raw of data.invites) {
+ const result : {id : _espe.type.invite_id; key : _espe.type.invite_key;} = await _espe.service.invite.create(
+ {
+ "membership_number_changeable": invite_raw.membership_number_changeable,
+ "membership_number_value": invite_raw.membership_number_value,
+ "name_changeable": invite_raw.name_changeable,
+ "name_value": invite_raw.name_value,
+ "email_address_changeable": invite_raw.email_address_changeable,
+ "email_address_value": invite_raw.email_address_value,
+ "groups_changeable": invite_raw.groups_changeable,
+ "groups_value": invite_raw.groups_value,
+ }
+ );
+ }
+ }
+ }
+
+
+ /**
+ */
+ export async function fill_by_path(
+ path : string
+ ) : Promise
+ {
+ const content : string = await lib_plankton.file.read(path);
+ const data : type_data = (lib_plankton.json.decode(content) as type_data);
+ await fill(data);
+ }
+
+}
+
diff --git a/source/services/invite.ts b/source/services/invite.ts
new file mode 100644
index 0000000..7629633
--- /dev/null
+++ b/source/services/invite.ts
@@ -0,0 +1,230 @@
+/*
+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
+.
+ */
+
+namespace _espe.service.invite
+{
+
+ /**
+ */
+ export function list(
+ ) : Promise<
+ Array<
+ {
+ id : _espe.type.invite_id;
+ key : _espe.type.invite_key;
+ expiry : (null | int);
+ name_value : string;
+ }
+ >
+ >
+ {
+ return (
+ _espe.repository.invite.dump()
+ .then(
+ entries => Promise.resolve(
+ entries.map(
+ entry => ({
+ "id": entry.id,
+ "key": entry.object.key,
+ "expiry": entry.object.expiry,
+ "name_value": entry.object.name_value
+ })
+ )
+ )
+ )
+ );
+ }
+
+
+ /**
+ */
+ export async function create(
+ {
+ "membership_number_changeable": membership_number_changeable,
+ "membership_number_value": membership_number_value,
+ "name_changeable": name_changeable,
+ "name_value": name_value,
+ "email_address_changeable": email_address_changeable,
+ "email_address_value": email_address_value,
+ "groups_changeable": groups_changeable,
+ "groups_value": groups_value,
+ } : {
+ membership_number_changeable : boolean;
+ membership_number_value : (null | string);
+ name_changeable : boolean;
+ name_value : string;
+ email_address_changeable : boolean;
+ email_address_value : (null | string);
+ groups_changeable : boolean;
+ groups_value : Array;
+ },
+ {
+ "expiry": expiry = -1,
+ } : {
+ expiry ?: (null | int);
+ } = {
+ }
+ ) : Promise<{id : _espe.type.invite_id; key : _espe.type.invite_key}>
+ {
+ /**
+ * @todo outsource to conf
+ */
+ const default_lifetime : int = (60 * 60 * 24 * 7 * 2);
+ /**
+ * @todo proper salt
+ */
+ const invite_key : _espe.type.invite_key = lib_plankton.sha256.get(
+ (
+ name_value
+ +
+ "/"
+ +
+ lib_plankton.base.get_current_timestamp(true).toFixed(0)
+ ),
+ "secret"
+ );
+ const invite_object : _espe.type.invite_object = {
+ "key": invite_key,
+ "expiry": (
+ ((expiry !== null) && (expiry < 0))
+ ?
+ (lib_plankton.base.get_current_timestamp(true) + default_lifetime)
+ :
+ expiry
+ ),
+ "membership_number_changeable": membership_number_changeable,
+ "membership_number_value": membership_number_value,
+ "name_changeable": name_changeable,
+ "name_value": name_value,
+ "email_address_changeable": email_address_changeable,
+ "email_address_value": email_address_value,
+ "groups_changeable": groups_changeable,
+ "groups_value": groups_value,
+ };
+ const invite_id : _espe.type.invite_id = await _espe.repository.invite.create(invite_object);
+ return {
+ "id": invite_id,
+ "key": invite_key,
+ };
+ }
+
+
+ /**
+ */
+ function get(
+ key : _espe.type.invite_key
+ ) : Promise<_espe.type.invite_object>
+ {
+ return (
+ _espe.repository.invite.identify(key)
+ .then(
+ (id) => _espe.repository.invite.read(id)
+ )
+ );
+ }
+
+
+ /**
+ */
+ export async function examine(
+ key : _espe.type.invite_key
+ ) : Promise<_espe.type.invite_object>
+ {
+ let invite_object : (null | _espe.type.invite_object);
+ try {
+ invite_object = await get(key);
+ }
+ catch (error) {
+ invite_object = null;
+ }
+ if (invite_object === null) {
+ return Promise.reject(new Error("not found"))
+ }
+ else {
+ const now : int = lib_plankton.base.get_current_timestamp(true);
+ if ((invite_object.expiry !== null) && (invite_object.expiry < now)) {
+ return Promise.reject(new Error("expired"));
+ }
+ else {
+ return Promise.resolve(invite_object);
+ }
+ }
+ }
+
+
+ /**
+ * @todo heed expiry
+ * @todo password?
+ */
+ export async function accept(
+ key : _espe.type.invite_key,
+ membership_number_value : (null | string),
+ name_value : (null | string),
+ email_address_value : (null | string),
+ groups_value : Array
+ ) : Promise
+ {
+ const invite_id : _espe.type.invite_id = await _espe.repository.invite.identify(key);
+ const invite_object : _espe.type.invite_object = await _espe.repository.invite.read(invite_id);
+ const now : int = lib_plankton.base.get_current_timestamp(true);
+ if ((invite_object.expiry !== null) && (invite_object.expiry < now)) {
+ return Promise.reject(new Error("expired"));
+ }
+ else {
+ const member_id : _espe.type.member_id = await _espe.service.member.project(
+ {
+ "membership_number": (
+ invite_object.membership_number_changeable
+ ?
+ membership_number_value
+ :
+ invite_object.membership_number_value
+ ),
+ "name_real_value": (
+ (
+ invite_object.name_changeable
+ &&
+ (name_value !== null)
+ )
+ ?
+ name_value
+ :
+ invite_object.name_value
+ ),
+ "email_address_private": (
+ (
+ invite_object.email_address_changeable
+ &&
+ (email_address_value !== null)
+ )
+ ?
+ email_address_value
+ :
+ invite_object.email_address_value
+ ),
+ "groups": (
+ invite_object.groups_changeable
+ ?
+ groups_value
+ :
+ invite_object.groups_value
+ ),
+ }
+ );
+ await _espe.repository.invite.delete_(invite_id);
+ }
+ }
+
+}
diff --git a/source/services/member.ts b/source/services/member.ts
index 9ee771c..d8d4733 100644
--- a/source/services/member.ts
+++ b/source/services/member.ts
@@ -310,6 +310,12 @@ namespace _espe.service.member
name_real_value : string;
email_address_private : (null | string);
groups : Array;
+ },
+ {
+ "silent": silent = false,
+ } : {
+ silent ?: boolean;
+ } = {
}
) : Promise<_espe.type.member_id>
{
@@ -331,7 +337,12 @@ namespace _espe.service.member
"groups": data.groups,
};
const id : _espe.type.member_id = await _espe.repository.member.create(object);
- signal_change();
+ if (silent) {
+ // do nothing
+ }
+ else {
+ signal_change();
+ }
return id;
}
diff --git a/source/types.ts b/source/types.ts
index aa8c6c4..441bddf 100644
--- a/source/types.ts
+++ b/source/types.ts
@@ -55,4 +55,30 @@ namespace _espe.type
password_change_token : (null | string);
};
+
+ /**
+ */
+ export type invite_id = int;
+
+
+ /**
+ */
+ export type invite_key = string;
+
+
+ /**
+ */
+ export type invite_object = {
+ key : invite_key;
+ expiry : (null | int);
+ membership_number_changeable : boolean;
+ membership_number_value : (null | string);
+ name_changeable : boolean;
+ name_value : string;
+ email_address_changeable : boolean;
+ email_address_value : (null | string);
+ groups_changeable : boolean;
+ groups_value : Array;
+ };
+
}
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..3a53d88
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,2 @@
+- Niederschreiben (Logging) von geheimen Angaben verhindern
+
diff --git a/tools/makefile b/tools/makefile
index 9a44cc8..f1554e2 100644
--- a/tools/makefile
+++ b/tools/makefile
@@ -51,9 +51,11 @@ ${dir_temp}/espe-core.js ${dir_temp}/espe-core.d.ts: \
${dir_source}/repositories/admin.ts \
${dir_source}/repositories/name_index.ts \
${dir_source}/repositories/member.ts \
+ ${dir_source}/repositories/invite.ts \
+ ${dir_source}/services/admin.ts \
${dir_source}/services/name_index.ts \
${dir_source}/services/member.ts \
- ${dir_source}/services/admin.ts \
+ ${dir_source}/services/invite.ts \
${dir_source}/api/base.ts \
${dir_source}/api/actions/meta_ping.ts \
${dir_source}/api/actions/meta_spec.ts \
@@ -69,6 +71,10 @@ ${dir_temp}/espe-core.js ${dir_temp}/espe-core.d.ts: \
${dir_source}/api/actions/member_delete.ts \
${dir_source}/api/actions/member_password_change_initialize.ts \
${dir_source}/api/actions/member_password_change_execute.ts \
+ ${dir_source}/api/actions/invite_list.ts \
+ ${dir_source}/api/actions/invite_create.ts \
+ ${dir_source}/api/actions/invite_examine.ts \
+ ${dir_source}/api/actions/invite_accept.ts \
${dir_source}/api/functions.ts \
${dir_source}/conf.ts
@ ${cmd_log} "compile | core …"
@@ -81,6 +87,7 @@ main: core ${dir_build}/espe data
${dir_temp}/espe-main-raw.js: \
${dir_lib}/plankton/plankton.d.ts \
${dir_temp}/espe-core.d.ts \
+ ${dir_source}/sample.ts \
${dir_source}/main.ts
@ ${cmd_log} "compile | main …"
@ ${cmd_mkdir} $(dir $@)