From 45acbbbcd9c3d81836f465f3b8b2740f0c98c648 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 11:33:41 +0200 Subject: [PATCH 1/6] [task-416] [upd] plankton --- lib/plankton/plankton.d.ts | 4 ++ lib/plankton/plankton.js | 22 +++++++-- source/value_objects/access_level.ts | 74 ---------------------------- 3 files changed, 22 insertions(+), 78 deletions(-) delete mode 100644 source/value_objects/access_level.ts diff --git a/lib/plankton/plankton.d.ts b/lib/plankton/plankton.d.ts index 49fd537..d07a527 100644 --- a/lib/plankton/plankton.d.ts +++ b/lib/plankton/plankton.d.ts @@ -658,6 +658,9 @@ declare namespace lib_plankton.call { /** */ export function sleep(seconds: float): Promise; + /** + */ + export function null_prop(value_from: (null | type_value_from), function_: ((value: type_value_from) => type_value_to)): (null | type_value_to); export {}; } declare namespace lib_plankton.email { @@ -4598,6 +4601,7 @@ declare namespace lib_plankton.auth.oidc { name: (null | string); label: (null | string); email: (null | string); + groups: (null | Array); }; /** */ diff --git a/lib/plankton/plankton.js b/lib/plankton/plankton.js index 2232892..3cf2c27 100644 --- a/lib/plankton/plankton.js +++ b/lib/plankton/plankton.js @@ -1491,6 +1491,16 @@ var lib_plankton; })); } call.sleep = sleep; + /** + */ + function null_prop(value_from, function_) { + return ((value_from === null) + ? + null + : + function_(value_from)); + } + call.null_prop = null_prop; })(call = lib_plankton.call || (lib_plankton.call = {})); })(lib_plankton || (lib_plankton = {})); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { @@ -4056,10 +4066,10 @@ var lib_plankton; options = Object.assign({ "compare_value": instance_compare }, options); - if (is_empty(list)) { + /*if (is_empty(list)) { throw (new Error("the max-arg of an empty list is not defined")); } - else { + else */ { return (list .reduce(function (result, element, index) { var value = target_function(element); @@ -14466,8 +14476,7 @@ var lib_plankton; } }; lib_plankton.log.info("plankton.server.client_connected"); - socket.on("data", (input_chunk_raw, x2, x3, x4, x5) => { - process.stderr.write(JSON.stringify({ x2, x3, x4, x5 }) + "\n"); + socket.on("data", (input_chunk_raw) => { lib_plankton.log.debug("plankton.server.reading_chunk", { "chunk_raw": ((input_chunk_raw instanceof Buffer) ? @@ -16231,6 +16240,11 @@ var lib_plankton; "name": (data["preferred_username"] ?? null), "label": (data["name"] ?? null), "email": (data["email"] ?? null), + "groups": (((data["groups"] === undefined) || (data["groups"] === null)) + ? + null + : + data["groups"].split(",")), }); } /** diff --git a/source/value_objects/access_level.ts b/source/value_objects/access_level.ts deleted file mode 100644 index d1a5168..0000000 --- a/source/value_objects/access_level.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* -This file is part of »zeitbild«. - -Copyright 2025 'kcf' - -»zeitbild« is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -»zeitbild« 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 Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with »zeitbild«. If not, see . - */ - - -/** - */ -namespace _zeitbild.value_object.access_level -{ - - /** - */ - export function to_string( - access_level : _zeitbild.enum_access_level - ) : string - { - switch (access_level) { - case _zeitbild.enum_access_level.none: {return "none";} - case _zeitbild.enum_access_level.view: {return "view";} - case _zeitbild.enum_access_level.edit: {return "edit";} - case _zeitbild.enum_access_level.admin: {return "admin";} - default: {throw (new Error("invalid access level: " + String(access_level)));} - } - } - - - /** - */ - export function from_string( - access_level_ : string - ) : _zeitbild.enum_access_level - { - switch (access_level_) { - case "none": {return _zeitbild.enum_access_level.none;} - case "view": {return _zeitbild.enum_access_level.view;} - case "edit": {return _zeitbild.enum_access_level.edit;} - case "admin": {return _zeitbild.enum_access_level.admin;} - default: {throw (new Error("invalid encoded access level: " + String(access_level_)));} - } - } - - - /** - */ - export function order( - x : _zeitbild.enum_access_level, - y : _zeitbild.enum_access_level - ) : boolean - { - const list : Array<_zeitbild.enum_access_level> = [ - _zeitbild.enum_access_level.none, - _zeitbild.enum_access_level.view, - _zeitbild.enum_access_level.edit, - _zeitbild.enum_access_level.admin, - ]; - return (list.indexOf(x) <= list.indexOf(y)); - } - -} -- 2.47.3 From 78729c611134d02496d899c522929f4797a20655 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 11:34:00 +0200 Subject: [PATCH 2/6] [task-416] --- misc/data-house_and_garden.json | 56 ++- source/api/actions/calendar_add.ts | 32 +- source/api/actions/calendar_change.ts | 32 +- source/api/actions/calendar_get.ts | 29 +- source/api/actions/calendar_list.ts | 2 +- source/api/actions/group_list.ts | 110 +++++ source/api/actions/session_oidc.ts | 74 ++++ source/api/functions.ts | 4 + source/auth.ts | 3 + source/database.ts | 2 +- source/logic.ts | 199 +++++++++ source/repositories/calendar.ts | 380 +++++++++++++----- source/repositories/group.ts | 207 ++++++++++ source/repositories/sql/calendar_overview.sql | 20 +- source/repositories/user.ts | 234 +++++++++-- source/sample.ts | 260 +++++++----- source/services/calendar.ts | 62 ++- source/services/group.ts | 63 +++ source/types.ts | 22 +- tools/makefile | 5 +- 20 files changed, 1494 insertions(+), 302 deletions(-) create mode 100644 source/api/actions/group_list.ts create mode 100644 source/logic.ts create mode 100644 source/repositories/group.ts create mode 100644 source/services/group.ts diff --git a/misc/data-house_and_garden.json b/misc/data-house_and_garden.json index fd63989..1f8044c 100644 --- a/misc/data-house_and_garden.json +++ b/misc/data-house_and_garden.json @@ -1,18 +1,40 @@ { + "groups": [ + { + "id": 1, + "name": "gaertner", + "label": "Gärtner" + }, + { + "id": 2, + "name": "bewohner", + "label": "Bewohner" + } + ], "users": [ { "id": 1, "name": "alice", + "groups": [1], "email_address": "alice@example.org", - "dav_token": "alice_dav", + "dav_token": null, "password": "alice" }, { "id": 2, "name": "bob", + "groups": [2], "email_address": "bob@example.org", "dav_token": null, "password": "bob" + }, + { + "id": 3, + "name": "charlie", + "groups": [1, 2], + "email_address": "charlie@example.org", + "dav_token": "charlie_dav", + "password": "charlie" } ], "calendars": [ @@ -22,8 +44,14 @@ "hue": 0.0000, "access": { "public": true, - "default_level": "edit", - "attributed": [ + "default_level": "view", + "attributed_group": [ + ], + "attributed_user": [ + { + "user_id": 3, + "level": "admin" + } ] }, "resource": { @@ -86,14 +114,16 @@ "access": { "public": false, "default_level": "none", - "attributed": [ + "attributed_group": [ + { + "group_id": 1, + "level": "view" + } + ], + "attributed_user": [ { "user_id": 1, "level": "admin" - }, - { - "user_id": 2, - "level": "view" } ] }, @@ -204,11 +234,13 @@ "access": { "public": false, "default_level": "none", - "attributed": [ + "attributed_group": [ { - "user_id": 1, - "level": "none" - }, + "group_id": 2, + "level": "view" + } + ], + "attributed_user": [ { "user_id": 2, "level": "admin" diff --git a/source/api/actions/calendar_add.ts b/source/api/actions/calendar_add.ts index f664f23..c829196 100644 --- a/source/api/actions/calendar_add.ts +++ b/source/api/actions/calendar_add.ts @@ -33,7 +33,13 @@ namespace _zeitbild.api access : { public : boolean; default_level : string; - attributed : Array< + attributed_group : Array< + { + group_id : int; + level : string; + } + >; + attributed_user : Array< { user_id : int; level : string; @@ -112,17 +118,33 @@ namespace _zeitbild.api "name": stuff.input.name, "access": { "public": stuff.input.access.public, - "default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level), - "attributed": lib_plankton.map.hashmap.implementation_map( + "default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level), + "attributed_group": lib_plankton.map.hashmap.implementation_map( lib_plankton.map.hashmap.make( x => x.toFixed(0), { "pairs": ( - stuff.input.access.attributed + stuff.input.access.attributed_group + .map( + (entry) => ({ + "key": entry.group_id, + "value": _zeitbild.access_level_from_string(entry.level), + }) + ) + ), + } + ) + ), + "attributed_user": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + stuff.input.access.attributed_user .map( (entry) => ({ "key": entry.user_id, - "value": _zeitbild.value_object.access_level.from_string(entry.level), + "value": _zeitbild.access_level_from_string(entry.level), }) ) .concat( diff --git a/source/api/actions/calendar_change.ts b/source/api/actions/calendar_change.ts index bb41388..71792cc 100644 --- a/source/api/actions/calendar_change.ts +++ b/source/api/actions/calendar_change.ts @@ -34,7 +34,13 @@ namespace _zeitbild.api access : { public : boolean; default_level : string; - attributed : Array< + attributed_group : Array< + { + group_id : int; + level : string; + } + >; + attributed_user : Array< { user_id : int; level : string; @@ -77,17 +83,33 @@ namespace _zeitbild.api "hue": stuff.input.hue, "access": { "public": stuff.input.access.public, - "default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level), - "attributed": lib_plankton.map.hashmap.implementation_map( + "default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level), + "attributed_group": lib_plankton.map.hashmap.implementation_map( lib_plankton.map.hashmap.make( x => x.toFixed(0), { "pairs": ( - stuff.input.access.attributed + stuff.input.access.attributed_group + .map( + (entry) => ({ + "key": entry.group_id, + "value": _zeitbild.access_level_from_string(entry.level), + }) + ) + ), + } + ) + ), + "attributed_user": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + stuff.input.access.attributed_user .map( (entry) => ({ "key": entry.user_id, - "value": _zeitbild.value_object.access_level.from_string(entry.level), + "value": _zeitbild.access_level_from_string(entry.level), }) ) ), diff --git a/source/api/actions/calendar_get.ts b/source/api/actions/calendar_get.ts index 6c7fa17..05245f1 100644 --- a/source/api/actions/calendar_get.ts +++ b/source/api/actions/calendar_get.ts @@ -35,12 +35,18 @@ namespace _zeitbild.api access : { public : boolean; default_level : string; - attributed : Array< + attributed_group : Array< + { + group_id : int; + level : string; + } + >; + attributed_user : Array< { user_id : int; level : string; } - > + >; }; resource_id : int; } @@ -69,8 +75,23 @@ namespace _zeitbild.api "access": { "public": calendar_object.access.public, "default_level": _zeitbild.api.access_level_encode(calendar_object.access.default_level), - "attributed": lib_plankton.call.convey( - calendar_object.access.attributed, + "attributed_group": lib_plankton.call.convey( + calendar_object.access.attributed_group, + [ + lib_plankton.map.dump, + (pairs : Array<{key : _zeitbild.type_group_id; value : _zeitbild.enum_access_level;}>) => ( + pairs + .map( + (pair : {key : _zeitbild.type_group_id; value : _zeitbild.enum_access_level;}) => ({ + "group_id": pair.key, + "level": _zeitbild.api.access_level_encode(pair.value) + }) + ) + ) + ] + ), + "attributed_user": lib_plankton.call.convey( + calendar_object.access.attributed_user, [ lib_plankton.map.dump, (pairs : Array<{key : _zeitbild.type_user_id; value : _zeitbild.enum_access_level;}>) => ( diff --git a/source/api/actions/calendar_list.ts b/source/api/actions/calendar_list.ts index c5b5e74..5e22c13 100644 --- a/source/api/actions/calendar_list.ts +++ b/source/api/actions/calendar_list.ts @@ -101,7 +101,7 @@ namespace _zeitbild.api "id": entry.id, "name": entry.name, "hue": entry.hue, - "access_level": _zeitbild.value_object.access_level.to_string(entry.access_level), + "access_level": _zeitbild.access_level_to_string(entry.access_level), }) ) ) diff --git a/source/api/actions/group_list.ts b/source/api/actions/group_list.ts new file mode 100644 index 0000000..04b289c --- /dev/null +++ b/source/api/actions/group_list.ts @@ -0,0 +1,110 @@ +/* +This file is part of »zeitbild«. + +Copyright 2025 'kcf' + +»zeitbild« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»zeitbild« 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »zeitbild«. If not, see . + */ + + +namespace _zeitbild.api +{ + + /** + */ + export function register_group_list( + rest_subject : lib_plankton.rest_http.type_rest + ) : void + { + register< + null, + Array< + { + id : int; + name : string; + label : string; + } + > + >( + rest_subject, + lib_plankton.http.enum_method.get, + "/groups", + { + "description": "listet alle Gruppen auf", + "query_parameters": () => ([ + { + "name": "term", + "required": false, + "description": "search term", + }, + ]), + "output_schema": () => ({ + "type": "array", + "items": { + "nullable": false, + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "nullable": false, + "type": "number", + }, + "name": { + "nullable": false, + "type": "string", + }, + "label": { + "nullable": false, + "type": "string", + }, + }, + "required": [ + "id", + "name", + "label", + ], + } + }), + "restriction": restriction_logged_in, + "execution": async (stuff) => { + const result : Array< + { + id : _zeitbild.type_group_id; + name : string; + label : string; + } + > = ( + (await _zeitbild.service.group.list()) + .map( + entry => ( + { + "id": entry.id, + "name": entry.object.name, + "label": entry.object.label, + } + ) + ) + ); + return Promise.resolve( + { + "status_code": 200, + "data": result, + } + ); + } + } + ); + } + +} diff --git a/source/api/actions/session_oidc.ts b/source/api/actions/session_oidc.ts index b4901d5..a5c59f1 100644 --- a/source/api/actions/session_oidc.ts +++ b/source/api/actions/session_oidc.ts @@ -21,6 +21,38 @@ along with »zeitbild«. If not, see . namespace _zeitbild.api { + /** + */ + function get_group_name( + group_name_raw : string + ) + : string + { + return lib_plankton.string.coin( + "auto-{{name_raw}}", + { + "name_raw": group_name_raw, + } + ); + } + + + /** + */ + function get_group_label( + group_name_raw : string + ) + : string + { + return lib_plankton.string.coin( + "{{name_raw}}", + { + "name_raw": group_name_raw, + } + ); + } + + /** */ export function register_session_oidc( @@ -74,6 +106,7 @@ namespace _zeitbild.api userinfo : { name : (null | string); email : (null | string); + groups : (null | Array); }; redirect_uri_template : string; } = await _zeitbild.auth.oidc_handle_authorization_callback( @@ -92,9 +125,50 @@ namespace _zeitbild.api { try { + // groups + const group_ids : Array<_zeitbild.type_group_id> = await Promise.all<_zeitbild.type_group_id>( + (data.userinfo.groups ?? []) + .map( + async (group_name_raw) => { + const group_name : string = get_group_name(group_name_raw); + let group_id : (null | _zeitbild.type_group_id) = await (() => { + try + { + return _zeitbild.repository.group.identify(group_name); + } + catch (error) + { + return Promise.resolve(null); + } + }) (); + if (group_id === null) + { + group_id = await _zeitbild.service.group.add( + { + "name": group_name, + "label": get_group_label(group_name_raw), + } + ); + return group_id; + } + else + { + await _zeitbild.service.group.change( + group_id, + { + "name": group_name, + "label": get_group_label(group_name_raw), + } + ); + return group_id; + } + } + ) + ); await _zeitbild.service.user.add( { "name": data.userinfo.name, + "groups": group_ids, "email_address": data.userinfo.email, "dav_token": null, } diff --git a/source/api/functions.ts b/source/api/functions.ts index c295f09..343ed4a 100644 --- a/source/api/functions.ts +++ b/source/api/functions.ts @@ -51,6 +51,10 @@ namespace _zeitbild.api _zeitbild.api.register_session_oidc(rest_subject); _zeitbild.api.register_session_status(rest_subject); } + // groups + { + _zeitbild.api.register_group_list(rest_subject); + } // user { _zeitbild.api.register_users(rest_subject); diff --git a/source/auth.ts b/source/auth.ts index ee40336..8e5ffd9 100644 --- a/source/auth.ts +++ b/source/auth.ts @@ -190,6 +190,7 @@ namespace _zeitbild.auth userinfo : { name : (null | string); email : (null | string); + groups : (null | Array); }; redirect_uri_template : string; } @@ -207,6 +208,7 @@ namespace _zeitbild.auth userinfo : { name : (null | string); email : (null | string); + groups : (null | Array); }; } = await lib_plankton.auth.oidc.handle_authorization_callback( _subject_oidc, @@ -219,6 +221,7 @@ namespace _zeitbild.auth userinfo : { name : (null | string); email : (null | string); + groups : (null | Array); }; redirect_uri_template : string; } diff --git a/source/database.ts b/source/database.ts index 9186408..0b9f47f 100644 --- a/source/database.ts +++ b/source/database.ts @@ -24,7 +24,7 @@ namespace _zeitbild.database /** */ const _compatible_revisions : Array = [ - "r5", + "r6", ]; diff --git a/source/logic.ts b/source/logic.ts new file mode 100644 index 0000000..888bd1b --- /dev/null +++ b/source/logic.ts @@ -0,0 +1,199 @@ +/* +This file is part of »zeitbild«. + +Copyright 2025 'kcf' + +»zeitbild« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»zeitbild« 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »zeitbild«. If not, see . + */ + + +/** + */ +namespace _zeitbild +{ + + + /** + */ + export function access_level_to_string( + access_level : _zeitbild.enum_access_level + ) + : string + { + switch (access_level) + { + case _zeitbild.enum_access_level.none: {return "none";} + case _zeitbild.enum_access_level.view: {return "view";} + case _zeitbild.enum_access_level.edit: {return "edit";} + case _zeitbild.enum_access_level.admin: {return "admin";} + default: {throw (new Error("invalid access level: " + String(access_level)));} + } + } + + + /** + */ + export function access_level_from_string( + representation : string + ) + : _zeitbild.enum_access_level + { + switch (representation) + { + case "none": {return _zeitbild.enum_access_level.none;} + case "view": {return _zeitbild.enum_access_level.view;} + case "edit": {return _zeitbild.enum_access_level.edit;} + case "admin": {return _zeitbild.enum_access_level.admin;} + default: {throw (new Error("invalid encoded access level: " + String(representation)));} + } + } + + + /** + */ + export function access_level_order( + x : _zeitbild.enum_access_level, + y : _zeitbild.enum_access_level + ) + : boolean + { + const list : Array<_zeitbild.enum_access_level> = [ + _zeitbild.enum_access_level.none, + _zeitbild.enum_access_level.view, + _zeitbild.enum_access_level.edit, + _zeitbild.enum_access_level.admin, + ]; + return (list.indexOf(x) <= list.indexOf(y)); + } + + + /** + */ + export function access_level_determine_raw( + public_ : boolean, + access_level_attributed : ( + null + | + { + default : _zeitbild.enum_access_level, + group : Array<_zeitbild.enum_access_level>; + user : (null | _zeitbild.enum_access_level); + } + ) + ) + : _zeitbild.enum_access_level + { + return lib_plankton.call.convey( + _zeitbild.enum_access_level.none, + [ + // if public + (x : _zeitbild.enum_access_level) => ( + public_ + ? + _zeitbild.enum_access_level.view + : + x + ), + // if logged in + (x : _zeitbild.enum_access_level) => ( + (access_level_attributed === null) + ? + x + : + lib_plankton.call.convey( + x, + [ + // default + (y : _zeitbild.enum_access_level) => access_level_attributed.default, + // group + (y : _zeitbild.enum_access_level) => ( + lib_plankton.call.null_prop( + lib_plankton.list.max<_zeitbild.enum_access_level, _zeitbild.enum_access_level>( + access_level_attributed.group, + z => z, + { + "compare_value": _zeitbild.access_level_order, + } + ), + z => z.value + ) + ?? + y + ), + // user + (y : _zeitbild.enum_access_level) => ( + (access_level_attributed.user === null) + ? + y + : + access_level_attributed.user + ), + ] + ) + ), + ] + ); + } + + + /** + */ + export function access_level_determine( + calendar_object : _zeitbild.type_calendar_object, + user : ( + null + | + { + id : _zeitbild.type_user_id; + object : _zeitbild.type_user_object; + } + ) + ) + : _zeitbild.enum_access_level + { + return access_level_determine_raw( + calendar_object.access.public, + ( + (user === null) + ? + null + : + { + "default": calendar_object.access.default_level, + "group": ( + user.object.groups + .map<(null | _zeitbild.enum_access_level)>( + group_id => ( + calendar_object.access.attributed_group.has(group_id) + ? + calendar_object.access.attributed_group.get(group_id) + : + null + ) + ) + .filter( + x => (x !== null) + ) + ), + "user": ( + lib_plankton.call.try_catch_wrap<_zeitbild.enum_access_level>( + () => calendar_object.access.attributed_user.get(user.id) + ).value + ) + } + ) + ); + } + +} diff --git a/source/repositories/calendar.ts b/source/repositories/calendar.ts index 9118972..db66714 100644 --- a/source/repositories/calendar.ts +++ b/source/repositories/calendar.ts @@ -34,7 +34,16 @@ namespace _zeitbild.repository.calendar /** */ - type type_access_attributed_row = { + type type_access_attributed_group_row = { + // calendar_id : int; + group_id : int; + level : int; + }; + + + /** + */ + type type_access_attributed_user_row = { // calendar_id : int; user_id : int; level : int; @@ -45,7 +54,8 @@ namespace _zeitbild.repository.calendar */ type type_dispersal = { core_row : type_core_row; - access_attributed_rows : Array; + access_attributed_group_rows : Array; + access_attributed_user_rows : Array; }; @@ -71,7 +81,22 @@ namespace _zeitbild.repository.calendar /** */ - var _access_attributed_chest : ( + var _access_attributed_group_chest : ( + null + | + 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 + > + ) = null; + + + /** + */ + var _access_attributed_user_chest : ( null | lib_plankton.storage.type_chest< @@ -87,7 +112,8 @@ namespace _zeitbild.repository.calendar /** */ function get_core_store( - ) : lib_plankton.storage.type_store< + ) + : lib_plankton.storage.type_store< _zeitbild.type_calendar_id, Record, {}, @@ -95,7 +121,8 @@ namespace _zeitbild.repository.calendar Record > { - if (_core_store === null) { + if (_core_store === null) + { _core_store = lib_plankton.storage.sql_table_autokey_store( { "database_implementation": _zeitbild.database.get_implementation(), @@ -104,7 +131,8 @@ namespace _zeitbild.repository.calendar } ); } - else { + else + { // do nothing } return _core_store; @@ -113,7 +141,7 @@ namespace _zeitbild.repository.calendar /** */ - function get_access_attributed_chest( + function get_access_attributed_group_chest( ) : lib_plankton.storage.type_chest< Array, Record, @@ -122,19 +150,51 @@ namespace _zeitbild.repository.calendar Record > { - if (_access_attributed_chest === null) { - _access_attributed_chest = lib_plankton.storage.sql_table_common.chest( + if (_access_attributed_group_chest === null) + { + _access_attributed_group_chest = lib_plankton.storage.sql_table_common.chest( { "database_implementation": _zeitbild.database.get_implementation(), - "table_name": "calendar_access_attributed", + "table_name": "calendar_access_attributed_group", + "key_names": ["calendar_id","group_id"], + } + ); + } + else + { + // do nothing + } + return _access_attributed_group_chest; + } + + + /** + */ + function get_access_attributed_user_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 + > + { + if (_access_attributed_user_chest === null) + { + _access_attributed_user_chest = lib_plankton.storage.sql_table_common.chest( + { + "database_implementation": _zeitbild.database.get_implementation(), + "table_name": "calendar_access_attributed_user", "key_names": ["calendar_id","user_id"], } ); } - else { + else + { // do nothing - } - return _access_attributed_chest; + } + return _access_attributed_user_chest; } @@ -176,7 +236,8 @@ namespace _zeitbild.repository.calendar */ function encode( object : _zeitbild.type_calendar_object - ) : type_dispersal + ) + : type_dispersal { return { "core_row": { @@ -186,8 +247,18 @@ namespace _zeitbild.repository.calendar "access_level_default": encode_access_level(object.access.default_level), "resource_id": object.resource_id, }, - "access_attributed_rows": ( - lib_plankton.map.dump(object.access.attributed) + "access_attributed_group_rows": ( + lib_plankton.map.dump(object.access.attributed_group) + .map( + ({"key": group_id, "value": level}) => ({ + // "calendar_id": calendar_id, + "group_id": group_id, + "level": encode_access_level(level), + }) + ) + ), + "access_attributed_user_rows": ( + lib_plankton.map.dump(object.access.attributed_user) .map( ({"key": user_id, "value": level}) => ({ // "calendar_id": calendar_id, @@ -204,7 +275,8 @@ namespace _zeitbild.repository.calendar */ function decode( dispersal : type_dispersal - ) : _zeitbild.type_calendar_object + ) + : _zeitbild.type_calendar_object { return { "name": dispersal.core_row.name, @@ -212,19 +284,38 @@ namespace _zeitbild.repository.calendar "access": { "public": dispersal.core_row.access_public, "default_level": decode_access_level(dispersal.core_row.access_level_default), - "attributed": lib_plankton.map.hashmap.implementation_map( + "attributed_group": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make<_zeitbild.type_group_id, _zeitbild.enum_access_level>( + x => x.toFixed(0), + { + "pairs": ( + dispersal.access_attributed_group_rows + .map( + (access_attributed_group_row) => ({ + // "calendar_id": access_attributed_group_row["calendar_id"], + // "key": access_attributed_group_row["preview"]["user_id"], + "key": access_attributed_group_row.group_id, + // "value": decode_access_level(access_attributed_group_row["preview"]["level"]), + "value": decode_access_level(access_attributed_group_row.level), + }) + ) + ), + } + ) + ), + "attributed_user": lib_plankton.map.hashmap.implementation_map( lib_plankton.map.hashmap.make<_zeitbild.type_user_id, _zeitbild.enum_access_level>( x => x.toFixed(0), { "pairs": ( - dispersal.access_attributed_rows + dispersal.access_attributed_user_rows .map( - (access_attributed_row) => ({ - // "calendar_id": access_attributed_row["calendar_id"], - // "key": access_attributed_row["preview"]["user_id"], - "key": access_attributed_row.user_id, - // "value": decode_access_level(access_attributed_row["preview"]["level"]), - "value": decode_access_level(access_attributed_row.level), + (access_attributed_user_row) => ({ + // "calendar_id": access_attributed_user_row["calendar_id"], + // "key": access_attributed_user_row["preview"]["user_id"], + "key": access_attributed_user_row.user_id, + // "value": decode_access_level(access_attributed_user_row["preview"]["level"]), + "value": decode_access_level(access_attributed_user_row.level), }) ) ), @@ -240,52 +331,67 @@ namespace _zeitbild.repository.calendar /** */ - export function read( + export async function read( id : _zeitbild.type_calendar_id - ) : Promise<_zeitbild.type_calendar_object> + ) + : Promise<_zeitbild.type_calendar_object> { - return ( - get_core_store().read(id) - .then( - (core_row_raw) => { - const core_row : type_core_row = (core_row_raw as type_core_row); - return ( - get_access_attributed_chest().search( - { - "expression": "(calendar_id = $calendar_id)", - "arguments": { - "calendar_id": id, - } - } - ) - .then( - (hits) => Promise.resolve( - { - "core_row": core_row, - "access_attributed_rows": ( - hits - .map( - hit => ( - { - // "calendar_id": null, - "user_id": hit.preview.user_id, - "level": hit.preview.level, - } - ) - ) - ), - } - ) - ) - .then( - (dispersal) => Promise.resolve<_zeitbild.type_calendar_object>( - decode(dispersal) - ) - ) - ); + const core_row : type_core_row = ((await get_core_store().read(id)) as type_core_row); + const access_attributed_group_rows : Array = await ( + get_access_attributed_group_chest().search( + { + "expression": "(calendar_id = $calendar_id)", + "arguments": { + "calendar_id": id, + } } ) + .then( + (hits) => Promise.resolve>( + hits + .map( + hit => ( + { + // "calendar_id": null, + "group_id": hit.preview.group_id, + "level": hit.preview.level, + } + ) + ) + ) + ) ); + const access_attributed_user_rows : Array = await ( + get_access_attributed_user_chest().search( + { + "expression": "(calendar_id = $calendar_id)", + "arguments": { + "calendar_id": id, + } + } + ) + .then( + (hits) => Promise.resolve>( + hits + .map( + hit => ( + { + // "calendar_id": null, + "user_id": hit.preview.user_id, + "level": hit.preview.level, + } + ) + ) + ) + ) + ); + const dispersal : type_dispersal = { + "core_row": core_row, + "access_attributed_group_rows": access_attributed_group_rows, + "access_attributed_user_rows": access_attributed_user_rows, + }; + const calendar_object : _zeitbild.type_calendar_object = decode(dispersal); + return calendar_object; } @@ -300,10 +406,16 @@ namespace _zeitbild.repository.calendar const calendar_id : _zeitbild.type_calendar_id = await core_store.create( dispersal.core_row ); - for await (const access_attributed_row of dispersal.access_attributed_rows) { - get_access_attributed_chest().write( - [calendar_id, access_attributed_row["user_id"]], - {"level": access_attributed_row["level"]} + for await (const access_attributed_group_row of dispersal.access_attributed_group_rows) { + get_access_attributed_group_chest().write( + [calendar_id, access_attributed_group_row["group_id"]], + {"level": access_attributed_group_row["level"]} + ); + } + for await (const access_attributed_user_row of dispersal.access_attributed_user_rows) { + get_access_attributed_user_chest().write( + [calendar_id, access_attributed_user_row["user_id"]], + {"level": access_attributed_user_row["level"]} ); } await lib_plankton.cache.clear(_zeitbild.cache_regular); @@ -327,10 +439,51 @@ namespace _zeitbild.repository.calendar dispersal.core_row ); } - // attributed access + // attributed:group { - const access_attributed_chest = get_access_attributed_chest(); - const hits : Array> = await access_attributed_chest.search( + const access_attributed_group_chest = get_access_attributed_group_chest(); + const hits : Array> = await access_attributed_group_chest.search( + { + "expression": "(calendar_id = $calendar_id)", + "arguments": { + "calendar_id": calendar_id, + } + } + ); + const contrast = lib_plankton.list.contrast< + Record, + Record + >( + hits, + hit => hit["group_id"], + dispersal.access_attributed_group_rows, + row => row["group_id"] + ); + // delete + for await (const entry of contrast.only_left) { + await access_attributed_group_chest.delete( + [calendar_id, entry.left["group_id"]] + ); + } + // update + for await (const entry of contrast.both) { + await access_attributed_group_chest.write( + [calendar_id, entry.right["group_id"]], + {"level": entry.right["level"]} + ); + } + // create + for await (const entry of contrast.only_right) { + await access_attributed_group_chest.write( + [calendar_id, entry.right["group_id"]], + {"level": entry.right["level"]} + ); + } + } + // attributed:user + { + const access_attributed_user_chest = get_access_attributed_user_chest(); + const hits : Array> = await access_attributed_user_chest.search( { "expression": "(calendar_id = $calendar_id)", "arguments": { @@ -344,25 +497,25 @@ namespace _zeitbild.repository.calendar >( hits, hit => hit["user_id"], - dispersal.access_attributed_rows, + dispersal.access_attributed_user_rows, row => row["user_id"] ); // delete for await (const entry of contrast.only_left) { - await access_attributed_chest.delete( + await access_attributed_user_chest.delete( [calendar_id, entry.left["user_id"]] ); } // update for await (const entry of contrast.both) { - await access_attributed_chest.write( + await access_attributed_user_chest.write( [calendar_id, entry.right["user_id"]], {"level": entry.right["level"]} ); } // create for await (const entry of contrast.only_right) { - await access_attributed_chest.write( + await access_attributed_user_chest.write( [calendar_id, entry.right["user_id"]], {"level": entry.right["level"]} ); @@ -384,15 +537,16 @@ namespace _zeitbild.repository.calendar { await lib_plankton.cache.clear(_zeitbild.cache_regular); const core_store = get_core_store(); - const access_attributed_chest = get_access_attributed_chest(); - // attributed access + const access_attributed_user_chest = get_access_attributed_user_chest(); + // attributed:user { + const chest = get_access_attributed_user_chest(); const hits : Array< { key : Array; preview : Record; } - > = await access_attributed_chest.search( + > = await chest.search( { "expression": "(calendar_id = $calendar_id)", "arguments": { @@ -402,9 +556,28 @@ namespace _zeitbild.repository.calendar ); for (const hit of hits) { - await access_attributed_chest.delete( - hit.key - ); + await chest.delete(hit.key); + } + } + // attributed:group + { + const chest = get_access_attributed_group_chest(); + const hits : Array< + { + key : Array; + preview : Record; + } + > = await chest.search( + { + "expression": "(calendar_id = $calendar_id)", + "arguments": { + "calendar_id": calendar_id, + } + } + ); + for (const hit of hits) + { + await chest.delete(hit.key); } } // core @@ -432,7 +605,8 @@ namespace _zeitbild.repository.calendar */ export async function overview( user_id : (null | _zeitbild.type_user_id) - ) : Promise< + ) + : Promise< Array< type_overview_entry > @@ -468,25 +642,37 @@ namespace _zeitbild.repository.calendar "name": row["name"], "hue": (row["hue"] / hue_scaling), /** - * @todo unite with _zeitbild.service.calendar.get_access_level + * @todo use _zeitbild.access_level_determine */ - "access_level": decode_access_level( - Math.max( - (row["access_public"] ? 1 : 0), - ( - (user_id === null) - ? - 0 - : - (row["access_level_attributed"] ?? row["access_level_default"]) - ) + "access_level": _zeitbild.access_level_determine_raw( + row["access_public"], + ( + (user_id === null) + ? + null + : + { + "default": decode_access_level(row["access_level_default"]), + "group": ( + lib_plankton.call.null_prop>( + row["access_level_attributed_group"], + x => x.split(",").map(parseInt).map(decode_access_level) + ) + ?? + [] + ), + "user": lib_plankton.call.null_prop( + row["access_level_attributed_user"], + decode_access_level + ), + } ) ), }) ), (x : Array) => x.filter( (row) => ( - ! _zeitbild.value_object.access_level.order( + ! _zeitbild.access_level_order( row.access_level, _zeitbild.enum_access_level.none ) @@ -499,7 +685,7 @@ namespace _zeitbild.repository.calendar row => row.access_level, row => row.id, { - "order_first": (a, b) => _zeitbild.value_object.access_level.order(b, a), + "order_first": (a, b) => _zeitbild.access_level_order(b, a), "order_second": (a, b) => (a <= b) } ), diff --git a/source/repositories/group.ts b/source/repositories/group.ts new file mode 100644 index 0000000..db1367e --- /dev/null +++ b/source/repositories/group.ts @@ -0,0 +1,207 @@ +/* +This file is part of »zeitbild«. + +Copyright 2025 'kcf' + +»zeitbild« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»zeitbild« 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »zeitbild«. If not, see . + */ + + +namespace _zeitbild.repository.group +{ + + /** + */ + type type_row = { + name : string; + label : string; + }; + + + /** + */ + type type_preview = { + name : string; + label : string; + }; + + + /** + */ + var _store : ( + null + | + lib_plankton.storage.type_store< + _zeitbild.type_user_id, + /*type_row*/Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + /*type_preview*/Record + > + ) = null; + + + /** + */ + function get_store( + ) + : lib_plankton.storage.type_store< + _zeitbild.type_user_id, + /*type_row*/Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + /*type_preview*/Record + > + { + if (_store === null) + { + _store = lib_plankton.storage.sql_table_autokey_store( + { + "database_implementation": _zeitbild.database.get_implementation(), + "table_name": "groups", + "key_name": "id", + } + ); + } + else + { + // do nothing + } + return _store; + } + + + /** + */ + function encode( + group_object : _zeitbild.type_group_object + ) + : type_row + { + return { + "name": group_object.name, + "label": group_object.label, + }; + } + + + /** + */ + function decode( + row : type_row + ) + : _zeitbild.type_group_object + { + return { + "name": row.name, + "label": row.label, + }; + } + + + /** + */ + export async function list( + ) + : Promise< + Array< + { + id : _zeitbild.type_group_id; + object : _zeitbild.type_group_object; + } + > + > + { + const hits : Array<{key : int; preview : /*type_preview*/Record;}> = await get_store().search({"expression": "TRUE", "arguments": {}}); + return Promise.resolve( + hits + .map( + (hit) => ({ + "id": hit.key, + "object": { + "name": hit.preview.name, + "label": hit.preview.label, + } + }) + ) + ); + } + + + /** + */ + export async function read( + group_id : _zeitbild.type_group_id + ) + : Promise<_zeitbild.type_group_object> + { + const row : type_row = ((await get_store().read(group_id)) as type_row); + const group_object : _zeitbild.type_group_object = decode(row); + return Promise.resolve<_zeitbild.type_group_object>(group_object); + } + + + /** + */ + export async function create( + group_object : _zeitbild.type_group_object + ) + : Promise<_zeitbild.type_group_id> + { + const row : type_row = encode(group_object); + const group_id : _zeitbild.type_group_id = await get_store().create(row); + return Promise.resolve<_zeitbild.type_group_id>(group_id); + } + + + /** + */ + export async function update( + group_id : _zeitbild.type_group_id, + group_object : _zeitbild.type_group_object + ) + : Promise + { + const row : type_row = encode(group_object); + await get_store().update(group_id, row); + return Promise.resolve(undefined); + } + + + /** + */ + export async function identify( + name : string + ) + : Promise<_zeitbild.type_group_id> + { + const hits : Array<{key : _zeitbild.type_group_id; preview : /*type_preview*/Record;}> = await get_store().search( + { + "expression": "(name = $name)", + "arguments": { + "name": name, + } + } + ); + if (hits.length <= 0) + { + return Promise.reject<_zeitbild.type_group_id>(new Error("not found")); + } + else + { + return Promise.resolve<_zeitbild.type_group_id>(hits[0].key); + } + } + +} diff --git a/source/repositories/sql/calendar_overview.sql b/source/repositories/sql/calendar_overview.sql index a6736d1..8290e9a 100644 --- a/source/repositories/sql/calendar_overview.sql +++ b/source/repositories/sql/calendar_overview.sql @@ -1,15 +1,15 @@ --- Für gewöhnlich würde man hier gruppieren. Aufgrund des UNIQUE-constraints in "calendar_access_attributed" ist das --- jedoch nicht nötig, da dadurch für jeden Eintrag in "calendar" mit gegebener "user_id" höchstens ein Eintrag in --- "calendar_access_attributed" passt und da es ein LEFT OUTER JOIN ist, wird es _genau_ ein Eintrag sein - SELECT x.id AS id, - x.name AS name, - x.hue AS hue, - x.access_public AS access_public, - x.access_level_default AS access_level_default, - y.level AS access_level_attributed + MAX(x.name) AS name, + MAX(x.hue) AS hue, + MAX(x.access_public) AS access_public, + MAX(x.access_level_default) AS access_level_default, + GROUP_CONCAT(y1.level, ',') AS access_level_attributed_group, + GROUP_CONCAT(y2.level, ',') AS access_level_attributed_user FROM calendars AS x - LEFT OUTER JOIN calendar_access_attributed AS y ON ((x.id = y.calendar_id) AND (y.user_id = $user_id)) + LEFT OUTER JOIN calendar_access_attributed_group AS y1 ON ((x.id = y1.calendar_id) AND (y1.group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))) + LEFT OUTER JOIN calendar_access_attributed_user AS y2 ON ((x.id = y2.calendar_id) AND (y2.user_id = $user_id)) +GROUP BY + x.id ; diff --git a/source/repositories/user.ts b/source/repositories/user.ts index 62b2cbf..941cc38 100644 --- a/source/repositories/user.ts +++ b/source/repositories/user.ts @@ -23,13 +23,28 @@ namespace _zeitbild.repository.user /** */ - type type_row = { + type type_core_row = { name : string; email_address : (null | string); dav_token : (null | string); }; + /** + */ + type type_group_row_slim = { + group_id : int; + }; + + + /** + */ + type type_group_row_fat = { + user_id : int; + group_id : int; + }; + + /** */ type type_preview = { @@ -39,12 +54,20 @@ namespace _zeitbild.repository.user /** */ - var _store : ( + type type_dispersal = { + core : type_core_row; + groups : Array; + }; + + + /** + */ + var _store_core : ( null | lib_plankton.storage.type_store< _zeitbild.type_user_id, - /*type_row*/Record, + /*type_core_row*/Record, {}, lib_plankton.storage.type_sql_table_autokey_search_term, /*type_preview*/Record @@ -54,19 +77,34 @@ namespace _zeitbild.repository.user /** */ - function get_store( + var _store_groups : ( + null + | + lib_plankton.storage.type_store< + int, + /*type_group_row_fat*/Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + Record + > + ) = null; + + + /** + */ + function get_store_core( ) : lib_plankton.storage.type_store< _zeitbild.type_user_id, - /*type_row*/Record, + /*type_core_row*/Record, {}, lib_plankton.storage.type_sql_table_autokey_search_term, /*type_preview*/Record > { - if (_store === null) + if (_store_core === null) { - _store = lib_plankton.storage.sql_table_autokey_store( + _store_core = lib_plankton.storage.sql_table_autokey_store( { "database_implementation": _zeitbild.database.get_implementation(), "table_name": "users", @@ -77,8 +115,38 @@ namespace _zeitbild.repository.user else { // do nothing - } - return _store; + } + return _store_core; + } + + + /** + */ + function get_store_groups( + ) + : lib_plankton.storage.type_store< + int, + /*type_group_row_fat*/Record, + {}, + lib_plankton.storage.type_sql_table_autokey_search_term, + Record + > + { + if (_store_groups === null) + { + _store_groups = lib_plankton.storage.sql_table_autokey_store( + { + "database_implementation": _zeitbild.database.get_implementation(), + "table_name": "user_groups", + "key_name": "id", + } + ); + } + else + { + // do nothing + } + return _store_groups; } @@ -87,12 +155,23 @@ namespace _zeitbild.repository.user function encode( user_object : _zeitbild.type_user_object ) - : type_row + : type_dispersal { return { - "name": user_object.name, - "email_address": user_object.email_address, - "dav_token": user_object.dav_token, + "core": { + "name": user_object.name, + "email_address": user_object.email_address, + "dav_token": user_object.dav_token, + }, + "groups": ( + user_object.groups.map( + group_id => ( + { + "group_id": group_id, + } + ) + ) + ), }; } @@ -100,14 +179,19 @@ namespace _zeitbild.repository.user /** */ function decode( - row : type_row + dispersal : type_dispersal ) : _zeitbild.type_user_object { return { - "name": row.name, - "email_address": row.email_address, - "dav_token": row.dav_token, + "name": dispersal.core.name, + "groups": ( + dispersal.groups.map( + group_row => group_row.group_id, + ) + ), + "email_address": dispersal.core.email_address, + "dav_token": dispersal.core.dav_token, }; } @@ -125,7 +209,12 @@ namespace _zeitbild.repository.user > > { - const hits : Array<{key : int; preview : /*type_preview*/Record;}> = await get_store().search({"expression": "TRUE", "arguments": {}}); + const hits : Array<{key : int; preview : /*type_preview*/Record;}> = await get_store_core().search( + { + "expression": "TRUE", + "arguments": {} + } + ); return Promise.resolve( hits .map( @@ -145,8 +234,40 @@ namespace _zeitbild.repository.user ) : Promise<_zeitbild.type_user_object> { - const row : type_row = ((await get_store().read(user_id)) as type_row); - const user_object : _zeitbild.type_user_object = decode(row); + const core_row : type_core_row = ((await get_store_core().read(user_id)) as type_core_row); + const group_rows : Array = ( + ( + await get_store_groups().search( + { + "expression": "(user_id = $user_id)", + "arguments": { + "user_id": user_id, + } + } + ) + ) + .map( + hit => ( + { + "user_id": hit.preview.user_id, + "group_id": hit.preview.group_id, + } + ) + ) + ); + const dispersal : type_dispersal = { + "core": core_row, + "groups": ( + group_rows.map( + group_row_fat => ( + { + "group_id": group_row_fat.group_id, + } + ) + ) + ) + }; + const user_object : _zeitbild.type_user_object = decode(dispersal); return Promise.resolve<_zeitbild.type_user_object>(user_object); } @@ -158,8 +279,22 @@ namespace _zeitbild.repository.user ) : Promise<_zeitbild.type_user_id> { - const row : type_row = encode(user_object); - const user_id : _zeitbild.type_user_id = await get_store().create(row); + const dispersal : type_dispersal = encode(user_object); + // core + const user_id : _zeitbild.type_user_id = await (() => { + return get_store_core().create(dispersal.core); + }) (); + // groups + { + for (const group_row_slim of dispersal.groups) + { + const group_row_fat : type_group_row_fat = { + "user_id": user_id, + "group_id": group_row_slim.group_id, + }; + await get_store_groups().create(group_row_fat); + } + } return Promise.resolve<_zeitbild.type_user_id>(user_id); } @@ -172,8 +307,57 @@ namespace _zeitbild.repository.user ) : Promise { - const row : type_row = encode(user_object); - await get_store().update(user_id, row); + const dispersal : type_dispersal = encode(user_object); + // core + { + await get_store_core().update(user_id, dispersal.core); + } + // groups + { + const hits : Array<{key : int; preview : Record;}> = await get_store_groups().search( + { + "expression": "(user_id = $user_id)", + "arguments": { + "user_id": user_id, + } + } + ); + const contrast = lib_plankton.list.contrast( + hits, + hit => hit.preview.group_id.toFixed(0), + dispersal.groups, + group_row_slim => group_row_slim.group_id.toFixed(0) + ); + // delete + { + for (const entry of contrast.only_left) + { + await get_store_groups().delete(entry.left.key); + } + } + // update + { + for (const entry of contrast.both) + { + const row_group_fat : type_group_row_fat = { + "user_id": user_id, + "group_id": entry.right.group_id, + }; + await get_store_groups().update(entry.left.key, row_group_fat); + } + } + // create + { + for (const entry of contrast.only_right) + { + const row_group_fat : type_group_row_fat = { + "user_id": user_id, + "group_id": entry.right.group_id, + }; + await get_store_groups().create(row_group_fat); + } + } + } return Promise.resolve(undefined); } @@ -185,7 +369,7 @@ namespace _zeitbild.repository.user ) : Promise<_zeitbild.type_user_id> { - const hits : Array<{key : _zeitbild.type_user_id; preview : /*type_preview*/Record;}> = await get_store().search( + const hits : Array<{key : _zeitbild.type_user_id; preview : /*type_preview*/Record;}> = await get_store_core().search( { "expression": "(name = $name)", "arguments": { diff --git a/source/sample.ts b/source/sample.ts index 0a9ae0a..4cbe509 100644 --- a/source/sample.ts +++ b/source/sample.ts @@ -91,12 +91,20 @@ namespace _zeitbild.sample /** */ type type_data = { + groups : Array< + { + id : int; + name : string; + label ?: string; + } + >; users : Array< { id : int; name : string; + groups ?: Array; email_address : string; - dav_token : (null | string); + dav_token ?: (null | string); password : string; } >; @@ -107,7 +115,13 @@ namespace _zeitbild.sample access : { public ?: boolean; default_level : ("none" | "view" | "edit" | "admin"); - attributed : Array< + attributed_group ?: Array< + { + group_id : int; + level : ("none" | "view" | "edit" | "admin"); + } + >; + attributed_user ?: Array< { user_id : int; level : ("none" | "view" | "edit" | "admin"); @@ -222,6 +236,10 @@ namespace _zeitbild.sample ) : Promise { let track : { + group : Record< + int, + _zeitbild.type_group_id + >; user : Record< int, _zeitbild.type_user_id @@ -231,115 +249,153 @@ namespace _zeitbild.sample _zeitbild.type_user_id >; } = { + "group": {}, "user": {}, "calendar": {}, }; - for await (const user_raw of data.users) + // groups { - const user_object : _zeitbild.type_user_object = { - "name": user_raw.name, - "email_address": user_raw.email_address, - "dav_token": user_raw.dav_token, - }; - const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( - user_object - ); - await _zeitbild.service.auth_internal.set( - user_raw.name, - user_raw.password - ); - track.user[user_raw.id] = user_id; - } - for await (const calendar_raw of data.calendars) - { - let resource_object : _zeitbild.type_resource_object; - let resource_id : _zeitbild.type_resource_id; - switch (calendar_raw.resource.kind) + for await (const group_raw of data.groups) { - case "local": - { - resource_object = { - "kind": "local", - "data": { - "event_ids": [], - } - }; - resource_id = await _zeitbild.service.resource.add( - resource_object - ); - /*const event_ids : Array<_zeitbild.type_local_resource_event_id> = */await Promise.all( - calendar_raw.resource.data.events - .map( - (event_raw) => { - const event : _zeitbild.type_event_object = { - "name": event_raw.name, - "begin": decode_datetime(event_raw.begin), - "end": ( - (event_raw.end === null) - ? - null - : - decode_datetime(event_raw.end) - ), - "location": event_raw.location, - "link": event_raw.link, - "description": event_raw.description, - }; - return _zeitbild.service.resource.event_add(resource_id, event); - } - ) - ); - break; - } - case "ics_feed": - { - resource_object = { - "kind": "ics_feed", - "data": { - "url": calendar_raw.resource.data.url, - "from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false), - } - }; - resource_id = await _zeitbild.service.resource.add( - resource_object - ); - break; - } + const group_object : _zeitbild.type_group_object = { + "name": group_raw.name, + "label": (group_raw.label ?? group_raw.name), + }; + const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add( + group_object + ); + track.group[group_raw.id] = group_id; } - const calendar_object : _zeitbild.type_calendar_object = + } + // users + { + for await (const user_raw of data.users) { - "name": calendar_raw.name, - "hue": ( - calendar_raw.hue - ?? - ((calendar_raw.id * phi) % 1) - ), - "access": { - "public": (calendar_raw.access.public ?? false), - "default_level": _zeitbild.value_object.access_level.from_string(calendar_raw.access.default_level), - "attributed": lib_plankton.map.hashmap.implementation_map( - lib_plankton.map.hashmap.make( - x => x.toFixed(0), - { - "pairs": ( - calendar_raw.access.attributed - .map( - (entry) => ({ - "key": track.user[entry.user_id], - "value": _zeitbild.value_object.access_level.from_string(entry.level), - }) - ) - ), + const user_object : _zeitbild.type_user_object = { + "name": user_raw.name, + "groups": (user_raw.groups ?? []), + "email_address": user_raw.email_address, + "dav_token": (user_raw.dav_token ?? null), + }; + const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( + user_object + ); + await _zeitbild.service.auth_internal.set( + user_raw.name, + user_raw.password + ); + track.user[user_raw.id] = user_id; + } + } + // calendars + { + for await (const calendar_raw of data.calendars) + { + let resource_object : _zeitbild.type_resource_object; + let resource_id : _zeitbild.type_resource_id; + switch (calendar_raw.resource.kind) + { + case "local": + { + resource_object = { + "kind": "local", + "data": { + "event_ids": [], } - ) + }; + resource_id = await _zeitbild.service.resource.add( + resource_object + ); + /*const event_ids : Array<_zeitbild.type_local_resource_event_id> = */await Promise.all( + calendar_raw.resource.data.events + .map( + (event_raw) => { + const event : _zeitbild.type_event_object = { + "name": event_raw.name, + "begin": decode_datetime(event_raw.begin), + "end": ( + (event_raw.end === null) + ? + null + : + decode_datetime(event_raw.end) + ), + "location": event_raw.location, + "link": event_raw.link, + "description": event_raw.description, + }; + return _zeitbild.service.resource.event_add(resource_id, event); + } + ) + ); + break; + } + case "ics_feed": + { + resource_object = { + "kind": "ics_feed", + "data": { + "url": calendar_raw.resource.data.url, + "from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false), + } + }; + resource_id = await _zeitbild.service.resource.add( + resource_object + ); + break; + } + } + const calendar_object : _zeitbild.type_calendar_object = + { + "name": calendar_raw.name, + "hue": ( + calendar_raw.hue + ?? + ((calendar_raw.id * phi) % 1) ), - }, - "resource_id": resource_id, - }; - const calendar_id : _zeitbild.type_calendar_id = await _zeitbild.service.calendar.add( - calendar_object - ); - track.calendar[calendar_raw.id] = calendar_id; + "access": { + "public": (calendar_raw.access.public ?? false), + "default_level": _zeitbild.access_level_from_string(calendar_raw.access.default_level), + "attributed_group": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + (calendar_raw.access.attributed_group ?? []) + .map( + (entry) => ({ + "key": track.user[entry.group_id], + "value": _zeitbild.access_level_from_string(entry.level), + }) + ) + ), + } + ) + ), + "attributed_user": lib_plankton.map.hashmap.implementation_map( + lib_plankton.map.hashmap.make( + x => x.toFixed(0), + { + "pairs": ( + (calendar_raw.access.attributed_user ?? []) + .map( + (entry) => ({ + "key": track.user[entry.user_id], + "value": _zeitbild.access_level_from_string(entry.level), + }) + ) + ), + } + ) + ), + }, + "resource_id": resource_id, + }; + const calendar_id : _zeitbild.type_calendar_id = await _zeitbild.service.calendar.add( + calendar_object + ); + track.calendar[calendar_raw.id] = calendar_id; + } } return Promise.resolve(undefined); } diff --git a/source/services/calendar.ts b/source/services/calendar.ts index 2603f2d..637837a 100644 --- a/source/services/calendar.ts +++ b/source/services/calendar.ts @@ -23,41 +23,24 @@ namespace _zeitbild.service.calendar /** */ - function get_access_level( + async function get_access_level( calendar_object : _zeitbild.type_calendar_object, user_id : (null | _zeitbild.type_user_id) - ) : _zeitbild.enum_access_level + ) + : Promise<_zeitbild.enum_access_level> { - return ( - lib_plankton.list.max<_zeitbild.enum_access_level, _zeitbild.enum_access_level>( - [ - ( - calendar_object.access.public - ? - _zeitbild.enum_access_level.view - : - _zeitbild.enum_access_level.none - ), - ( - (user_id === null) - ? - _zeitbild.enum_access_level.none - : - calendar_object.access.attributed.get( - user_id, - lib_plankton.pod.make_filled<_zeitbild.enum_access_level>( - calendar_object.access.default_level - ) - ) - ), - ], - x => x, + return _zeitbild.access_level_determine( + calendar_object, + ( + (user_id === null) + ? + null + : { - "compare_value": _zeitbild.value_object.access_level.order, + "id": user_id, + "object": (await _zeitbild.service.user.get(user_id)), } - )?.value - ?? - _zeitbild.enum_access_level.none + ) ); } @@ -65,7 +48,7 @@ namespace _zeitbild.service.calendar /** * checks if a user has a sufficient access level */ - function wrap_check_access_level( + async function wrap_check_access_level( calendar_object : _zeitbild.type_calendar_object, user_id : (null | _zeitbild.type_user_id), threshold : _zeitbild.enum_access_level, @@ -74,26 +57,29 @@ namespace _zeitbild.service.calendar => Promise ) - ) : Promise + ) + : Promise { - const access_level : _zeitbild.enum_access_level = get_access_level( + const access_level : _zeitbild.enum_access_level = await get_access_level( calendar_object, user_id ); - if (! _zeitbild.value_object.access_level.order(threshold, access_level)) { + if (! _zeitbild.access_level_order(threshold, access_level)) + { return Promise.reject( new Error( lib_plankton.string.coin( "insufficient access level; at least required: {{threshold}}, actual: {{actual}}", { - "threshold": _zeitbild.value_object.access_level.to_string(threshold), - "actual": _zeitbild.value_object.access_level.to_string(access_level), + "threshold": _zeitbild.access_level_to_string(threshold), + "actual": _zeitbild.access_level_to_string(access_level), } ) ) ); } - else { + else + { return success_handler(access_level); } } @@ -579,7 +565,7 @@ namespace _zeitbild.service.calendar const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read( calendar_id ); - const access_level : _zeitbild.enum_access_level = get_access_level( + const access_level : _zeitbild.enum_access_level = await get_access_level( calendar_object, user_id ); diff --git a/source/services/group.ts b/source/services/group.ts new file mode 100644 index 0000000..cc9dde0 --- /dev/null +++ b/source/services/group.ts @@ -0,0 +1,63 @@ +/* +This file is part of »zeitbild«. + +Copyright 2025 'kcf' + +»zeitbild« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»zeitbild« 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »zeitbild«. If not, see . + */ + + +namespace _zeitbild.service.group +{ + + /** + */ + export function list( + ) + : Promise< + Array< + { + id : _zeitbild.type_group_id; + object : _zeitbild.type_group_object; + } + > + > + { + return _zeitbild.repository.group.list(); + } + + + /** + */ + export function add( + group_object : _zeitbild.type_group_object + ) + : Promise<_zeitbild.type_group_id> + { + return _zeitbild.repository.group.create(group_object); + } + + + /** + */ + export function change( + group_id : _zeitbild.type_group_id, + group_object : _zeitbild.type_group_object + ) + : Promise + { + return _zeitbild.repository.group.update(group_id, group_object); + } + +} diff --git a/source/types.ts b/source/types.ts index d917bef..be3c8db 100644 --- a/source/types.ts +++ b/source/types.ts @@ -33,6 +33,19 @@ namespace _zeitbild } + /** + */ + export type type_group_id = int; + + + /** + */ + export type type_group_object = { + name : string; + label : string; + }; + + /** */ export type type_user_id = int; @@ -42,6 +55,9 @@ namespace _zeitbild */ export type type_user_object = { name : string; + groups : Array< + type_group_id + >; email_address : ( null | @@ -131,7 +147,11 @@ namespace _zeitbild access : { public : boolean; default_level : enum_access_level; - attributed : lib_plankton.map.type_map< + attributed_group : lib_plankton.map.type_map< + type_group_id, + enum_access_level + >; + attributed_user : lib_plankton.map.type_map< type_user_id, enum_access_level >; diff --git a/tools/makefile b/tools/makefile index 9866701..d894ee2 100644 --- a/tools/makefile +++ b/tools/makefile @@ -47,12 +47,14 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/database.ts \ ${dir_source}/auth.ts \ ${dir_source}/types.ts \ - ${dir_source}/value_objects/access_level.ts \ + ${dir_source}/logic.ts \ ${dir_source}/repositories/auth_internal.ts \ + ${dir_source}/repositories/group.ts \ ${dir_source}/repositories/user.ts \ ${dir_source}/repositories/resource.ts \ ${dir_source}/repositories/calendar.ts \ ${dir_source}/services/auth_internal.ts \ + ${dir_source}/services/group.ts \ ${dir_source}/services/user.ts \ ${dir_source}/services/resource.ts \ ${dir_source}/services/calendar.ts \ @@ -65,6 +67,7 @@ ${dir_temp}/zeitbild-unlinked.js: \ ${dir_source}/api/actions/session_oidc.ts \ ${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/session_status.ts \ + ${dir_source}/api/actions/group_list.ts \ ${dir_source}/api/actions/users.ts \ ${dir_source}/api/actions/user_dav_conf.ts \ ${dir_source}/api/actions/user_dav_token.ts \ -- 2.47.3 From 3c49c744b3ff5bb3250b9edc3586461793d05e2e Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 13:17:10 +0200 Subject: [PATCH 3/6] [task-416] [upd] plankton --- lib/plankton/plankton.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/plankton/plankton.js b/lib/plankton/plankton.js index 3cf2c27..ee90b69 100644 --- a/lib/plankton/plankton.js +++ b/lib/plankton/plankton.js @@ -16240,11 +16240,7 @@ var lib_plankton; "name": (data["preferred_username"] ?? null), "label": (data["name"] ?? null), "email": (data["email"] ?? null), - "groups": (((data["groups"] === undefined) || (data["groups"] === null)) - ? - null - : - data["groups"].split(",")), + "groups": (data["groups"] ?? null), }); } /** -- 2.47.3 From 2eed983131323d1c241a1768fa0e7fb604c8138e Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 13:18:25 +0200 Subject: [PATCH 4/6] [task-416] [mod] oidc group handling [mod] calendar overview query handling (PostgreSQL specific) --- source/api/actions/session_oidc.ts | 132 ++++++++++-------- source/auth.ts | 1 + source/repositories/calendar.ts | 57 ++++++-- source/repositories/sql/calendar_overview.sql | 12 +- 4 files changed, 127 insertions(+), 75 deletions(-) diff --git a/source/api/actions/session_oidc.ts b/source/api/actions/session_oidc.ts index a5c59f1..fee9dda 100644 --- a/source/api/actions/session_oidc.ts +++ b/source/api/actions/session_oidc.ts @@ -123,67 +123,87 @@ namespace _zeitbild.api } else { - try - { - // groups - const group_ids : Array<_zeitbild.type_group_id> = await Promise.all<_zeitbild.type_group_id>( - (data.userinfo.groups ?? []) - .map( - async (group_name_raw) => { - const group_name : string = get_group_name(group_name_raw); - let group_id : (null | _zeitbild.type_group_id) = await (() => { - try + // groups + const group_ids : Array<_zeitbild.type_group_id> = await Promise.all<_zeitbild.type_group_id>( + (data.userinfo.groups ?? []) + .map( + async (group_name_raw) => { + const group_name : string = get_group_name(group_name_raw); + const group_id_raw : (null | _zeitbild.type_group_id) = await ( + _zeitbild.repository.group.identify(group_name) + .catch(() => Promise.resolve(null)) + ); + if (group_id_raw === null) + { + const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add( { - return _zeitbild.repository.group.identify(group_name); + "name": group_name, + "label": get_group_label(group_name_raw), } - catch (error) - { - return Promise.resolve(null); - } - }) (); - if (group_id === null) - { - group_id = await _zeitbild.service.group.add( - { - "name": group_name, - "label": get_group_label(group_name_raw), - } - ); - return group_id; - } - else - { - await _zeitbild.service.group.change( - group_id, - { - "name": group_name, - "label": get_group_label(group_name_raw), - } - ); - return group_id; - } + ); + return group_id; + } + else + { + const group_id : _zeitbild.type_group_id = group_id_raw; + await _zeitbild.service.group.change( + group_id, + { + "name": group_name, + "label": get_group_label(group_name_raw), + } + ); + return group_id; } - ) - ); - await _zeitbild.service.user.add( - { - "name": data.userinfo.name, - "groups": group_ids, - "email_address": data.userinfo.email, - "dav_token": null, } + ) + ); + + const user_id : _zeitbild.type_user_id = await (async () => { + const user_object : _zeitbild.type_user_object = { + "name": (data.userinfo.name as string), + "groups": group_ids, + "email_address": data.userinfo.email, + "dav_token": null, + }; + const user_id_raw : (null | _zeitbild.type_user_id) = await ( + _zeitbild.service.user.identify(data.userinfo.name as string) + .catch(() => Promise.resolve(null)) ); - lib_plankton.log.info( - "user_provisioned", - { - "name": data.userinfo.name, - } - ); - } - catch (error) - { - // do nothing - } + if (user_id_raw === null) + { + // provision + const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( + user_object + ); + lib_plankton.log.info( + "user_provisioned", + { + "id": user_id, + "name": user_object.name, + } + ); + return user_id; + } + else + { + const user_id : _zeitbild.type_user_id = user_id_raw; + // update + await _zeitbild.service.user.change( + user_id, + user_object + ); + lib_plankton.log.info( + "user_updated", + { + "id": user_id, + "name": user_object.name, + } + ); + return user_id; + } + }) (); + const session_key : string = await lib_plankton.session.begin( data.userinfo.name, { diff --git a/source/auth.ts b/source/auth.ts index 8e5ffd9..cfd094b 100644 --- a/source/auth.ts +++ b/source/auth.ts @@ -100,6 +100,7 @@ namespace _zeitbild.auth "openid", "profile", "email", + "groups", ], "label": _zeitbild.conf.get().authentication.data.label, } diff --git a/source/repositories/calendar.ts b/source/repositories/calendar.ts index db66714..03e54e3 100644 --- a/source/repositories/calendar.ts +++ b/source/repositories/calendar.ts @@ -639,31 +639,62 @@ namespace _zeitbild.repository.calendar (x : Array>) => x.map( (row : Record) => ({ "id": row["id"], - "name": row["name"], - "hue": (row["hue"] / hue_scaling), + "name": lib_plankton.call.convey( + row["name"], + [ + // JSON.parse, + (x : Array) => x[0], + ] + ), + "hue": lib_plankton.call.convey( + row["hue"], + [ + // JSON.parse, + (x : Array) => x[0], + (x : int) => (x / hue_scaling), + ] + ), /** * @todo use _zeitbild.access_level_determine */ "access_level": _zeitbild.access_level_determine_raw( - row["access_public"], + lib_plankton.call.convey( + row["access_public"], + [ + // JSON.parse, + (x : Array) => x[0], + ] + ), ( (user_id === null) ? null : { - "default": decode_access_level(row["access_level_default"]), - "group": ( - lib_plankton.call.null_prop>( - row["access_level_attributed_group"], - x => x.split(",").map(parseInt).map(decode_access_level) - ) - ?? - [] + "default": lib_plankton.call.convey( + row["access_level_default"], + [ + // JSON.parse, + (x : Array) => x[0], + decode_access_level, + ] ), - "user": lib_plankton.call.null_prop( + "group": lib_plankton.call.convey( + row["access_level_attributed_group"], + [ + // JSON.parse, + (x : Array<(null | int)>) => x.filter(y => (y !== null)), + (x : Array) => x.map(decode_access_level), + ] + ), + "user": lib_plankton.call.convey( row["access_level_attributed_user"], - decode_access_level + [ + // JSON.parse, + (x : Array<(null | int)>) => x.filter(y => (y !== null)), + (x : Array) => x.map(decode_access_level), + (x : Array<_zeitbild.enum_access_level>) => ((x.length > 0) ? x[0] : null), + ] ), } ) diff --git a/source/repositories/sql/calendar_overview.sql b/source/repositories/sql/calendar_overview.sql index 8290e9a..70a55b6 100644 --- a/source/repositories/sql/calendar_overview.sql +++ b/source/repositories/sql/calendar_overview.sql @@ -1,11 +1,11 @@ SELECT x.id AS id, - MAX(x.name) AS name, - MAX(x.hue) AS hue, - MAX(x.access_public) AS access_public, - MAX(x.access_level_default) AS access_level_default, - GROUP_CONCAT(y1.level, ',') AS access_level_attributed_group, - GROUP_CONCAT(y2.level, ',') AS access_level_attributed_user + JSON_AGG(x.name) AS name, + JSON_AGG(x.hue) AS hue, + JSON_AGG(x.access_public) AS access_public, + JSON_AGG(x.access_level_default) AS access_level_default, + JSON_AGG(y1.level) AS access_level_attributed_group, + JSON_AGG(y2.level) AS access_level_attributed_user FROM calendars AS x LEFT OUTER JOIN calendar_access_attributed_group AS y1 ON ((x.id = y1.calendar_id) AND (y1.group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))) -- 2.47.3 From 24322588d706c725ce790282013a5d3016f713cc Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 13:19:04 +0200 Subject: [PATCH 5/6] [fix] tools:deploy --- tools/deploy | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/deploy b/tools/deploy index a96b777..a2f2179 100755 --- a/tools/deploy +++ b/tools/deploy @@ -47,6 +47,7 @@ def main(): "--verbose", "--exclude='conf.json'", "--exclude='data.sqlite'", + "--exclude='log.jsonl'", ("%s/" % args.build_directory), ( ("%s" % args.target_directory) -- 2.47.3 From bef69283f62bfdd53fda30399122ea5e0eb74ec1 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Thu, 23 Oct 2025 19:08:00 +0200 Subject: [PATCH 6/6] [task-416] [mod] repository:calendar:sonder-query los werden --- lib/plankton/plankton.d.ts | 2 +- source/api/actions/session_oidc.ts | 179 +++----------- source/auth.ts | 158 ++++++++++-- source/repositories/calendar.ts | 226 ++++++++++-------- source/repositories/sql/calendar_overview.sql | 15 -- tools/makefile | 9 +- 6 files changed, 299 insertions(+), 290 deletions(-) delete mode 100644 source/repositories/sql/calendar_overview.sql diff --git a/lib/plankton/plankton.d.ts b/lib/plankton/plankton.d.ts index d07a527..8b1db32 100644 --- a/lib/plankton/plankton.d.ts +++ b/lib/plankton/plankton.d.ts @@ -4597,7 +4597,7 @@ declare namespace lib_plankton.auth.oidc { type type_token = string; /** */ - type type_userinfo = { + export type type_userinfo = { name: (null | string); label: (null | string); email: (null | string); diff --git a/source/api/actions/session_oidc.ts b/source/api/actions/session_oidc.ts index fee9dda..ebf7515 100644 --- a/source/api/actions/session_oidc.ts +++ b/source/api/actions/session_oidc.ts @@ -21,38 +21,6 @@ along with »zeitbild«. If not, see . namespace _zeitbild.api { - /** - */ - function get_group_name( - group_name_raw : string - ) - : string - { - return lib_plankton.string.coin( - "auto-{{name_raw}}", - { - "name_raw": group_name_raw, - } - ); - } - - - /** - */ - function get_group_label( - group_name_raw : string - ) - : string - { - return lib_plankton.string.coin( - "{{name_raw}}", - { - "name_raw": group_name_raw, - } - ); - } - - /** */ export function register_session_oidc( @@ -103,132 +71,39 @@ namespace _zeitbild.api "execution": async (stuff) => { const data : { token : string; - userinfo : { - name : (null | string); - email : (null | string); - groups : (null | Array); - }; + userinfo : lib_plankton.auth.oidc.type_userinfo; redirect_uri_template : string; } = await _zeitbild.auth.oidc_handle_authorization_callback( (stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null), stuff.query_parameters ); - if (data.userinfo.name === null) - { - return Promise.reject( - new Error( - "IDP did not return user name" - ) - ); - } - else - { - // groups - const group_ids : Array<_zeitbild.type_group_id> = await Promise.all<_zeitbild.type_group_id>( - (data.userinfo.groups ?? []) - .map( - async (group_name_raw) => { - const group_name : string = get_group_name(group_name_raw); - const group_id_raw : (null | _zeitbild.type_group_id) = await ( - _zeitbild.repository.group.identify(group_name) - .catch(() => Promise.resolve(null)) - ); - if (group_id_raw === null) - { - const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add( - { - "name": group_name, - "label": get_group_label(group_name_raw), - } - ); - return group_id; - } - else - { - const group_id : _zeitbild.type_group_id = group_id_raw; - await _zeitbild.service.group.change( - group_id, - { - "name": group_name, - "label": get_group_label(group_name_raw), - } - ); - return group_id; - } + + const user = await _zeitbild.auth.oidc_adapt_user(data.userinfo); + + const session_key : string = await lib_plankton.session.begin( + user.object.name, + { + "data": { + "oidc_token": data.token, + } + } + ); + return Promise.resolve( + { + "status_code": 200, + "data": lib_plankton.string.coin( + "", + { + "url": lib_plankton.string.coin( + data.redirect_uri_template, + { + "session_key": session_key, + } + ), } - ) - ); - - const user_id : _zeitbild.type_user_id = await (async () => { - const user_object : _zeitbild.type_user_object = { - "name": (data.userinfo.name as string), - "groups": group_ids, - "email_address": data.userinfo.email, - "dav_token": null, - }; - const user_id_raw : (null | _zeitbild.type_user_id) = await ( - _zeitbild.service.user.identify(data.userinfo.name as string) - .catch(() => Promise.resolve(null)) - ); - if (user_id_raw === null) - { - // provision - const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( - user_object - ); - lib_plankton.log.info( - "user_provisioned", - { - "id": user_id, - "name": user_object.name, - } - ); - return user_id; - } - else - { - const user_id : _zeitbild.type_user_id = user_id_raw; - // update - await _zeitbild.service.user.change( - user_id, - user_object - ); - lib_plankton.log.info( - "user_updated", - { - "id": user_id, - "name": user_object.name, - } - ); - return user_id; - } - }) (); - - const session_key : string = await lib_plankton.session.begin( - data.userinfo.name, - { - "data": { - "oidc_token": data.token, - } - } - ); - return Promise.resolve( - { - "status_code": 200, - "data": lib_plankton.string.coin( - "", - { - "url": lib_plankton.string.coin( - data.redirect_uri_template, - { - "session_key": session_key, - } - ), - } - ), - } - ); - } + ), + } + ); }, } ); diff --git a/source/auth.ts b/source/auth.ts index cfd094b..82b797b 100644 --- a/source/auth.ts +++ b/source/auth.ts @@ -188,11 +188,7 @@ namespace _zeitbild.auth ) : Promise< { token : string; - userinfo : { - name : (null | string); - email : (null | string); - groups : (null | Array); - }; + userinfo : lib_plankton.auth.oidc.type_userinfo; redirect_uri_template : string; } > @@ -206,11 +202,7 @@ namespace _zeitbild.auth const state : string = data["state"]; const result : { token : string; - userinfo : { - name : (null | string); - email : (null | string); - groups : (null | Array); - }; + userinfo : lib_plankton.auth.oidc.type_userinfo; } = await lib_plankton.auth.oidc.handle_authorization_callback( _subject_oidc, cookie, @@ -219,11 +211,7 @@ namespace _zeitbild.auth return Promise.resolve< { token : string; - userinfo : { - name : (null | string); - email : (null | string); - groups : (null | Array); - }; + userinfo : lib_plankton.auth.oidc.type_userinfo; redirect_uri_template : string; } >( @@ -236,4 +224,144 @@ namespace _zeitbild.auth } } + + /** + * @todo switch for enabling/disabling auto provisioning + */ + export async function oidc_adapt_user( + userinfo : lib_plankton.auth.oidc.type_userinfo + ) + : Promise< + { + id : _zeitbild.type_user_id; + object : _zeitbild.type_user_object; + } + > + { + if (userinfo.name === null) + { + return Promise.reject(new Error("IDP did not return user name")); + } + else + { + // groups + const group_ids : Array<_zeitbild.type_group_id> = await (async () => { + const derive_name : ((group_name_raw : string) => string) = ( + (group_name_raw) => lib_plankton.string.coin( + "auto-{{name_raw}}", + { + "name_raw": group_name_raw, + } + ) + ); + const derive_label : ((group_name_raw : string) => string) = ( + (group_name_raw) => lib_plankton.string.coin( + "{{name_raw}}", + { + "name_raw": group_name_raw, + } + ) + ); + return Promise.all<_zeitbild.type_group_id>( + (userinfo.groups ?? []) + .map( + async (group_name_raw) => { + const group_name : string = derive_name(group_name_raw); + const group_id_raw : (null | _zeitbild.type_group_id) = await ( + _zeitbild.repository.group.identify(group_name) + .catch(() => Promise.resolve(null)) + ); + if (group_id_raw === null) + { + // create + const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add( + { + "name": group_name, + "label": derive_label(group_name_raw), + } + ); + lib_plankton.log.info( + "zeitbild.oidc_adapt_user.auto_provisioned_group", + { + "id": group_id, + "name": group_name, + } + ); + return group_id; + } + else + { + // update + const group_id : _zeitbild.type_group_id = group_id_raw; + await _zeitbild.service.group.change( + group_id, + { + "name": group_name, + "label": derive_label(group_name_raw), + } + ); + return group_id; + } + } + ) + ); + }) (); + + // user + const user : { + id : _zeitbild.type_user_id; + object : _zeitbild.type_user_object; + } = await (async () => { + const user_id_raw : (null | _zeitbild.type_user_id) = await ( + _zeitbild.service.user.identify(userinfo.name as string) + .catch(() => Promise.resolve(null)) + ); + if (user_id_raw === null) + { + // provision + const user_object : _zeitbild.type_user_object = { + "name": (userinfo.name as string), + "groups": group_ids, + "email_address": userinfo.email, + "dav_token": null, + }; + const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add( + user_object + ); + lib_plankton.log.info( + "user_provisioned", + { + "id": user_id, + "name": user_object.name, + } + ); + return {"id": user_id, "object": user_object}; + } + else + { + // update + const user_id : _zeitbild.type_user_id = user_id_raw; + const user_object : _zeitbild.type_user_object = await _zeitbild.service.user.get(user_id); + user_object.name = (userinfo.name as string); + user_object.groups = group_ids; + user_object.email_address = userinfo.email; + await _zeitbild.service.user.change( + user_id, + user_object + ); + lib_plankton.log.info( + "user_updated", + { + "id": user_id, + "name": user_object.name, + } + ); + return {"id": user_id, "object": user_object}; + } + }) (); + + return user; + } + } + } diff --git a/source/repositories/calendar.ts b/source/repositories/calendar.ts index 03e54e3..43266df 100644 --- a/source/repositories/calendar.ts +++ b/source/repositories/calendar.ts @@ -328,7 +328,6 @@ namespace _zeitbild.repository.calendar } - /** */ export async function read( @@ -601,7 +600,6 @@ namespace _zeitbild.repository.calendar /** - * @todo caching */ export async function overview( user_id : (null | _zeitbild.type_user_id) @@ -612,122 +610,152 @@ namespace _zeitbild.repository.calendar > > { + type type_data = { + hits_core : Array< + { + key : int; + preview : Record; + } + >; + hits_access_attributed_group : Array< + { + key : int; + preview : Record; + } + >; + hits_access_attributed_user : Array< + { + key : int; + preview : Record; + } + >; + }; return lib_plankton.cache.get_complex>( _zeitbild.cache_regular, "calendar_overview", { "user_id": user_id, }, - null, - () => ( - lib_plankton.file.read("sql/calendar_overview.sql") - .then( - (template) => _zeitbild.database.get_implementation().query_free_get( + 60, + async () => lib_plankton.call.convey( + { + "hits_core": await get_core_store().search( { - "template": template, - "arguments": { - "user_id": user_id, - } + "expression": "TRUE", + "arguments": {} } - ) - ) - .then( - (rows) => Promise.resolve( - lib_plankton.call.convey( - rows, - [ - (x : Array>) => x.map( - (row : Record) => ({ - "id": row["id"], - "name": lib_plankton.call.convey( - row["name"], - [ - // JSON.parse, - (x : Array) => x[0], - ] - ), - "hue": lib_plankton.call.convey( - row["hue"], - [ - // JSON.parse, - (x : Array) => x[0], - (x : int) => (x / hue_scaling), - ] - ), - /** - * @todo use _zeitbild.access_level_determine - */ - "access_level": _zeitbild.access_level_determine_raw( - lib_plankton.call.convey( - row["access_public"], + ), + "hits_access_attributed_group": await get_access_attributed_group_chest().search( + (user_id === null) + ? + { + "expression": "TRUE", + "arguments": {} + } + : + { + "expression": "(group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))", + "arguments": {"user_id": user_id} + } + ), + "hits_access_attributed_user": await get_access_attributed_user_chest().search( + (user_id === null) + ? + { + "expression": "TRUE", + "arguments": {} + } + : + { + "expression": "(user_id = $user_id)", + "arguments": {"user_id": user_id} + } + ), + }, + [ + // transform + (data : type_data) => data.hits_core.map( + (hit_core) => { + const calendar_id : _zeitbild.type_calendar_id = hit_core.key; + return { + "id": calendar_id, + "name": hit_core.preview["name"], + "hue": (hit_core.preview["hue"] / hue_scaling), + "access_level": _zeitbild.access_level_determine_raw( + hit_core.preview["access_public"], + ( + (user_id === null) + ? + null + : + { + "default": decode_access_level(hit_core.preview["access_level_default"]), + "group": lib_plankton.call.convey( + data.hits_access_attributed_group, [ - // JSON.parse, - (x : Array) => x[0], + (x : Array<{key : int; preview : Record}>) => x.filter( + hit_access_attributed_group => ( + (hit_access_attributed_group.preview.calendar_id === calendar_id) + ) + ), + (x : Array<{key : int; preview : Record}>) => x.map( + hit_access_attributed_group => hit_access_attributed_group.preview.level + ), + (x : Array) => x.map( + decode_access_level + ), ] ), - ( - (user_id === null) - ? - null - : - { - "default": lib_plankton.call.convey( - row["access_level_default"], - [ - // JSON.parse, - (x : Array) => x[0], - decode_access_level, - ] + "user": lib_plankton.call.convey( + data.hits_access_attributed_user, + [ + (x : Array<{key : int; preview : Record}>) => x.filter( + hits_access_attributed_user => ( + (hits_access_attributed_user.preview.calendar_id === calendar_id) + ) ), - "group": lib_plankton.call.convey( - row["access_level_attributed_group"], - [ - // JSON.parse, - (x : Array<(null | int)>) => x.filter(y => (y !== null)), - (x : Array) => x.map(decode_access_level), - ] + (x : Array<{key : int; preview : Record}>) => x.map( + hits_access_attributed_user => hits_access_attributed_user.preview.level ), - "user": lib_plankton.call.convey( - row["access_level_attributed_user"], - [ - // JSON.parse, - (x : Array<(null | int)>) => x.filter(y => (y !== null)), - (x : Array) => x.map(decode_access_level), - (x : Array<_zeitbild.enum_access_level>) => ((x.length > 0) ? x[0] : null), - ] + (x : Array) => x.map( + decode_access_level ), - } - ) - ), - }) - ), - (x : Array) => x.filter( - (row) => ( - ! _zeitbild.access_level_order( - row.access_level, - _zeitbild.enum_access_level.none - ) + (x : Array<_zeitbild.enum_access_level>) => (x[0] ?? null), + ] + ), + } ) ), - (x : Array) => lib_plankton.list.sorted( - x, - { - "compare_element": lib_plankton.order.order_lexicographic_pair_wrapped( - row => row.access_level, - row => row.id, - { - "order_first": (a, b) => _zeitbild.access_level_order(b, a), - "order_second": (a, b) => (a <= b) - } - ), - } - ), - ] + }; + } + ), + // only keep visible calendars + (x : Array) => x.filter( + (row) => ( + ! _zeitbild.access_level_order( + row.access_level, + _zeitbild.enum_access_level.none + ) ) - ) - ) + ), + // sort by access level and name + (x : Array) => lib_plankton.list.sorted( + x, + { + "compare_element": lib_plankton.order.order_lexicographic_pair_wrapped( + row => row.access_level, + row => row.id, + { + "order_first": (a, b) => _zeitbild.access_level_order(b, a), + "order_second": (a, b) => (a <= b) + } + ), + } + ), + ] ) ); } + } diff --git a/source/repositories/sql/calendar_overview.sql b/source/repositories/sql/calendar_overview.sql deleted file mode 100644 index 70a55b6..0000000 --- a/source/repositories/sql/calendar_overview.sql +++ /dev/null @@ -1,15 +0,0 @@ -SELECT - x.id AS id, - JSON_AGG(x.name) AS name, - JSON_AGG(x.hue) AS hue, - JSON_AGG(x.access_public) AS access_public, - JSON_AGG(x.access_level_default) AS access_level_default, - JSON_AGG(y1.level) AS access_level_attributed_group, - JSON_AGG(y2.level) AS access_level_attributed_user -FROM - calendars AS x - LEFT OUTER JOIN calendar_access_attributed_group AS y1 ON ((x.id = y1.calendar_id) AND (y1.group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))) - LEFT OUTER JOIN calendar_access_attributed_user AS y2 ON ((x.id = y2.calendar_id) AND (y2.user_id = $user_id)) -GROUP BY - x.id -; diff --git a/tools/makefile b/tools/makefile index d894ee2..bdbbd70 100644 --- a/tools/makefile +++ b/tools/makefile @@ -18,7 +18,7 @@ cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc ## rules .PHONY: default -default: node_modules sql ${dir_build}/zeitbild node_modules +default: node_modules ${dir_build}/zeitbild node_modules .PHONY: node_modules node_modules: @@ -26,13 +26,6 @@ node_modules: @ ${cmd_log} "node modules …" @ ${cmd_cp} -r -u ${dir_lib}/node/node_modules/* ${dir_build}/node_modules/ -.PHONY: sql -sql: \ - $(wildcard ${dir_source}/repositories/sql/*) - @ ${cmd_log} "sql …" - @ ${cmd_mkdir} ${dir_build}/sql - @ ${cmd_cp} -r -u $^ ${dir_build}/sql/ - ${dir_temp}/conf.ts: \ ${dir_source}/conf.ts.tpl \ ${dir_source}/conf.schema.json -- 2.47.3