Compare commits

..

No commits in common. "2c019723bf2cf15b312fb2fb9caf353c4b3a951c" and "2a672381ddf39ba617a1fa5ef9517d38368d1ff4" have entirely different histories.

23 changed files with 503 additions and 1696 deletions

View file

@ -658,9 +658,6 @@ declare namespace lib_plankton.call {
/** /**
*/ */
export function sleep(seconds: float): Promise<void>; export function sleep(seconds: float): Promise<void>;
/**
*/
export function null_prop<type_value_from, type_value_to>(value_from: (null | type_value_from), function_: ((value: type_value_from) => type_value_to)): (null | type_value_to);
export {}; export {};
} }
declare namespace lib_plankton.email { declare namespace lib_plankton.email {
@ -4597,11 +4594,10 @@ declare namespace lib_plankton.auth.oidc {
type type_token = string; type type_token = string;
/** /**
*/ */
export type type_userinfo = { type type_userinfo = {
name: (null | string); name: (null | string);
label: (null | string); label: (null | string);
email: (null | string); email: (null | string);
groups: (null | Array<string>);
}; };
/** /**
*/ */

View file

@ -1491,16 +1491,6 @@ var lib_plankton;
})); }));
} }
call.sleep = sleep; 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 = {})); })(call = lib_plankton.call || (lib_plankton.call = {}));
})(lib_plankton || (lib_plankton = {})); })(lib_plankton || (lib_plankton = {}));
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
@ -4066,10 +4056,10 @@ var lib_plankton;
options = Object.assign({ options = Object.assign({
"compare_value": instance_compare "compare_value": instance_compare
}, options); }, options);
/*if (is_empty<type_element>(list)) { if (is_empty(list)) {
throw (new Error("the max-arg of an empty list is not defined")); throw (new Error("the max-arg of an empty list is not defined"));
} }
else */ { else {
return (list return (list
.reduce(function (result, element, index) { .reduce(function (result, element, index) {
var value = target_function(element); var value = target_function(element);
@ -14476,7 +14466,8 @@ var lib_plankton;
} }
}; };
lib_plankton.log.info("plankton.server.client_connected"); lib_plankton.log.info("plankton.server.client_connected");
socket.on("data", (input_chunk_raw) => { socket.on("data", (input_chunk_raw, x2, x3, x4, x5) => {
process.stderr.write(JSON.stringify({ x2, x3, x4, x5 }) + "\n");
lib_plankton.log.debug("plankton.server.reading_chunk", { lib_plankton.log.debug("plankton.server.reading_chunk", {
"chunk_raw": ((input_chunk_raw instanceof Buffer) "chunk_raw": ((input_chunk_raw instanceof Buffer)
? ?
@ -16240,7 +16231,6 @@ var lib_plankton;
"name": (data["preferred_username"] ?? null), "name": (data["preferred_username"] ?? null),
"label": (data["name"] ?? null), "label": (data["name"] ?? null),
"email": (data["email"] ?? null), "email": (data["email"] ?? null),
"groups": (data["groups"] ?? null),
}); });
} }
/** /**

View file

@ -1,40 +1,18 @@
{ {
"groups": [
{
"id": 1,
"name": "gaertner",
"label": "Gärtner"
},
{
"id": 2,
"name": "bewohner",
"label": "Bewohner"
}
],
"users": [ "users": [
{ {
"id": 1, "id": 1,
"name": "alice", "name": "alice",
"groups": [1],
"email_address": "alice@example.org", "email_address": "alice@example.org",
"dav_token": null, "dav_token": "alice_dav",
"password": "alice" "password": "alice"
}, },
{ {
"id": 2, "id": 2,
"name": "bob", "name": "bob",
"groups": [2],
"email_address": "bob@example.org", "email_address": "bob@example.org",
"dav_token": null, "dav_token": null,
"password": "bob" "password": "bob"
},
{
"id": 3,
"name": "charlie",
"groups": [1, 2],
"email_address": "charlie@example.org",
"dav_token": "charlie_dav",
"password": "charlie"
} }
], ],
"calendars": [ "calendars": [
@ -44,14 +22,8 @@
"hue": 0.0000, "hue": 0.0000,
"access": { "access": {
"public": true, "public": true,
"default_level": "view", "default_level": "edit",
"attributed_group": [ "attributed": [
],
"attributed_user": [
{
"user_id": 3,
"level": "admin"
}
] ]
}, },
"resource": { "resource": {
@ -114,16 +86,14 @@
"access": { "access": {
"public": false, "public": false,
"default_level": "none", "default_level": "none",
"attributed_group": [ "attributed": [
{
"group_id": 1,
"level": "view"
}
],
"attributed_user": [
{ {
"user_id": 1, "user_id": 1,
"level": "admin" "level": "admin"
},
{
"user_id": 2,
"level": "view"
} }
] ]
}, },
@ -234,13 +204,11 @@
"access": { "access": {
"public": false, "public": false,
"default_level": "none", "default_level": "none",
"attributed_group": [ "attributed": [
{ {
"group_id": 2, "user_id": 1,
"level": "view" "level": "none"
} },
],
"attributed_user": [
{ {
"user_id": 2, "user_id": 2,
"level": "admin" "level": "admin"

View file

@ -33,13 +33,7 @@ namespace _zeitbild.api
access : { access : {
public : boolean; public : boolean;
default_level : string; default_level : string;
attributed_group : Array< attributed : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : string; level : string;
@ -118,33 +112,17 @@ namespace _zeitbild.api
"name": stuff.input.name, "name": stuff.input.name,
"access": { "access": {
"public": stuff.input.access.public, "public": stuff.input.access.public,
"default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level), "default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level),
"attributed_group": lib_plankton.map.hashmap.implementation_map( "attributed": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make( lib_plankton.map.hashmap.make(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
stuff.input.access.attributed_group stuff.input.access.attributed
.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( .map(
(entry) => ({ (entry) => ({
"key": entry.user_id, "key": entry.user_id,
"value": _zeitbild.access_level_from_string(entry.level), "value": _zeitbild.value_object.access_level.from_string(entry.level),
}) })
) )
.concat( .concat(

View file

@ -34,13 +34,7 @@ namespace _zeitbild.api
access : { access : {
public : boolean; public : boolean;
default_level : string; default_level : string;
attributed_group : Array< attributed : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : string; level : string;
@ -83,33 +77,17 @@ namespace _zeitbild.api
"hue": stuff.input.hue, "hue": stuff.input.hue,
"access": { "access": {
"public": stuff.input.access.public, "public": stuff.input.access.public,
"default_level": _zeitbild.access_level_from_string(stuff.input.access.default_level), "default_level": _zeitbild.value_object.access_level.from_string(stuff.input.access.default_level),
"attributed_group": lib_plankton.map.hashmap.implementation_map( "attributed": lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make( lib_plankton.map.hashmap.make(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
stuff.input.access.attributed_group stuff.input.access.attributed
.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( .map(
(entry) => ({ (entry) => ({
"key": entry.user_id, "key": entry.user_id,
"value": _zeitbild.access_level_from_string(entry.level), "value": _zeitbild.value_object.access_level.from_string(entry.level),
}) })
) )
), ),

View file

@ -35,18 +35,12 @@ namespace _zeitbild.api
access : { access : {
public : boolean; public : boolean;
default_level : string; default_level : string;
attributed_group : Array< attributed : Array<
{
group_id : int;
level : string;
}
>;
attributed_user : Array<
{ {
user_id : int; user_id : int;
level : string; level : string;
} }
>; >
}; };
resource_id : int; resource_id : int;
} }
@ -75,23 +69,8 @@ namespace _zeitbild.api
"access": { "access": {
"public": calendar_object.access.public, "public": calendar_object.access.public,
"default_level": _zeitbild.api.access_level_encode(calendar_object.access.default_level), "default_level": _zeitbild.api.access_level_encode(calendar_object.access.default_level),
"attributed_group": lib_plankton.call.convey( "attributed": lib_plankton.call.convey(
calendar_object.access.attributed_group, calendar_object.access.attributed,
[
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, lib_plankton.map.dump,
(pairs : Array<{key : _zeitbild.type_user_id; value : _zeitbild.enum_access_level;}>) => ( (pairs : Array<{key : _zeitbild.type_user_id; value : _zeitbild.enum_access_level;}>) => (

View file

@ -101,7 +101,7 @@ namespace _zeitbild.api
"id": entry.id, "id": entry.id,
"name": entry.name, "name": entry.name,
"hue": entry.hue, "hue": entry.hue,
"access_level": _zeitbild.access_level_to_string(entry.access_level), "access_level": _zeitbild.value_object.access_level.to_string(entry.access_level),
}) })
) )
) )

View file

@ -1,110 +0,0 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
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,
}
);
}
}
);
}
}

View file

@ -71,39 +71,70 @@ namespace _zeitbild.api
"execution": async (stuff) => { "execution": async (stuff) => {
const data : { const data : {
token : string; token : string;
userinfo : lib_plankton.auth.oidc.type_userinfo; userinfo : {
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} = await _zeitbild.auth.oidc_handle_authorization_callback( } = await _zeitbild.auth.oidc_handle_authorization_callback(
(stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null), (stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null),
stuff.query_parameters stuff.query_parameters
); );
if (data.userinfo.name === null)
const user = await _zeitbild.auth.oidc_adapt_user(data.userinfo); {
return Promise.reject(
const session_key : string = await lib_plankton.session.begin( new Error(
user.object.name, "IDP did not return user name"
)
);
}
else
{
try
{ {
"data": { await _zeitbild.service.user.add(
"oidc_token": data.token,
}
}
);
return Promise.resolve(
{
"status_code": 200,
"data": lib_plankton.string.coin(
"<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>",
{ {
"url": lib_plankton.string.coin( "name": data.userinfo.name,
data.redirect_uri_template, "email_address": data.userinfo.email,
{ "dav_token": null,
"session_key": session_key,
}
),
} }
), );
lib_plankton.log.info(
"user_provisioned",
{
"name": data.userinfo.name,
}
);
} }
); catch (error)
{
// do nothing
}
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(
"<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>",
{
"url": lib_plankton.string.coin(
data.redirect_uri_template,
{
"session_key": session_key,
}
),
}
),
}
);
}
}, },
} }
); );

View file

@ -51,10 +51,6 @@ namespace _zeitbild.api
_zeitbild.api.register_session_oidc(rest_subject); _zeitbild.api.register_session_oidc(rest_subject);
_zeitbild.api.register_session_status(rest_subject); _zeitbild.api.register_session_status(rest_subject);
} }
// groups
{
_zeitbild.api.register_group_list(rest_subject);
}
// user // user
{ {
_zeitbild.api.register_users(rest_subject); _zeitbild.api.register_users(rest_subject);

View file

@ -100,7 +100,6 @@ namespace _zeitbild.auth
"openid", "openid",
"profile", "profile",
"email", "email",
"groups",
], ],
"label": _zeitbild.conf.get().authentication.data.label, "label": _zeitbild.conf.get().authentication.data.label,
} }
@ -188,7 +187,10 @@ namespace _zeitbild.auth
) : Promise< ) : Promise<
{ {
token : string; token : string;
userinfo : lib_plankton.auth.oidc.type_userinfo; userinfo : {
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} }
> >
@ -202,7 +204,10 @@ namespace _zeitbild.auth
const state : string = data["state"]; const state : string = data["state"];
const result : { const result : {
token : string; token : string;
userinfo : lib_plankton.auth.oidc.type_userinfo; userinfo : {
name : (null | string);
email : (null | string);
};
} = await lib_plankton.auth.oidc.handle_authorization_callback( } = await lib_plankton.auth.oidc.handle_authorization_callback(
_subject_oidc, _subject_oidc,
cookie, cookie,
@ -211,7 +216,10 @@ namespace _zeitbild.auth
return Promise.resolve< return Promise.resolve<
{ {
token : string; token : string;
userinfo : lib_plankton.auth.oidc.type_userinfo; userinfo : {
name : (null | string);
email : (null | string);
};
redirect_uri_template : string; redirect_uri_template : string;
} }
>( >(
@ -224,144 +232,4 @@ 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;
}
}
} }

View file

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

View file

@ -1,199 +0,0 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
/**
*/
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
)
}
)
);
}
}

View file

@ -34,16 +34,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
type type_access_attributed_group_row = { type type_access_attributed_row = {
// calendar_id : int;
group_id : int;
level : int;
};
/**
*/
type type_access_attributed_user_row = {
// calendar_id : int; // calendar_id : int;
user_id : int; user_id : int;
level : int; level : int;
@ -54,8 +45,7 @@ namespace _zeitbild.repository.calendar
*/ */
type type_dispersal = { type type_dispersal = {
core_row : type_core_row; core_row : type_core_row;
access_attributed_group_rows : Array<type_access_attributed_group_row>; access_attributed_rows : Array<type_access_attributed_row>;
access_attributed_user_rows : Array<type_access_attributed_user_row>;
}; };
@ -81,22 +71,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
var _access_attributed_group_chest : ( var _access_attributed_chest : (
null
|
lib_plankton.storage.type_chest<
Array<any>,
Record<string, any>,
lib_plankton.database.type_description_create_table,
lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any>
>
) = null;
/**
*/
var _access_attributed_user_chest : (
null null
| |
lib_plankton.storage.type_chest< lib_plankton.storage.type_chest<
@ -112,8 +87,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_core_store( function get_core_store(
) ) : lib_plankton.storage.type_store<
: lib_plankton.storage.type_store<
_zeitbild.type_calendar_id, _zeitbild.type_calendar_id,
Record<string, any>, Record<string, any>,
{}, {},
@ -121,8 +95,7 @@ namespace _zeitbild.repository.calendar
Record<string, any> Record<string, any>
> >
{ {
if (_core_store === null) if (_core_store === null) {
{
_core_store = lib_plankton.storage.sql_table_autokey_store( _core_store = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
@ -131,8 +104,7 @@ namespace _zeitbild.repository.calendar
} }
); );
} }
else else {
{
// do nothing // do nothing
} }
return _core_store; return _core_store;
@ -141,7 +113,7 @@ namespace _zeitbild.repository.calendar
/** /**
*/ */
function get_access_attributed_group_chest( function get_access_attributed_chest(
) : lib_plankton.storage.type_chest< ) : lib_plankton.storage.type_chest<
Array<any>, Array<any>,
Record<string, any>, Record<string, any>,
@ -150,51 +122,19 @@ namespace _zeitbild.repository.calendar
Record<string, any> Record<string, any>
> >
{ {
if (_access_attributed_group_chest === null) if (_access_attributed_chest === null) {
{ _access_attributed_chest = lib_plankton.storage.sql_table_common.chest(
_access_attributed_group_chest = lib_plankton.storage.sql_table_common.chest(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "calendar_access_attributed_group", "table_name": "calendar_access_attributed",
"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<any>,
Record<string, any>,
lib_plankton.database.type_description_create_table,
lib_plankton.storage.sql_table_common.type_sql_table_common_search_term,
Record<string, any>
>
{
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"], "key_names": ["calendar_id","user_id"],
} }
); );
} }
else else {
{
// do nothing // do nothing
} }
return _access_attributed_user_chest; return _access_attributed_chest;
} }
@ -236,8 +176,7 @@ namespace _zeitbild.repository.calendar
*/ */
function encode( function encode(
object : _zeitbild.type_calendar_object object : _zeitbild.type_calendar_object
) ) : type_dispersal
: type_dispersal
{ {
return { return {
"core_row": { "core_row": {
@ -247,18 +186,8 @@ namespace _zeitbild.repository.calendar
"access_level_default": encode_access_level(object.access.default_level), "access_level_default": encode_access_level(object.access.default_level),
"resource_id": object.resource_id, "resource_id": object.resource_id,
}, },
"access_attributed_group_rows": ( "access_attributed_rows": (
lib_plankton.map.dump(object.access.attributed_group) lib_plankton.map.dump(object.access.attributed)
.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( .map(
({"key": user_id, "value": level}) => ({ ({"key": user_id, "value": level}) => ({
// "calendar_id": calendar_id, // "calendar_id": calendar_id,
@ -275,8 +204,7 @@ namespace _zeitbild.repository.calendar
*/ */
function decode( function decode(
dispersal : type_dispersal dispersal : type_dispersal
) ) : _zeitbild.type_calendar_object
: _zeitbild.type_calendar_object
{ {
return { return {
"name": dispersal.core_row.name, "name": dispersal.core_row.name,
@ -284,38 +212,19 @@ namespace _zeitbild.repository.calendar
"access": { "access": {
"public": dispersal.core_row.access_public, "public": dispersal.core_row.access_public,
"default_level": decode_access_level(dispersal.core_row.access_level_default), "default_level": decode_access_level(dispersal.core_row.access_level_default),
"attributed_group": lib_plankton.map.hashmap.implementation_map( "attributed": 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>( lib_plankton.map.hashmap.make<_zeitbild.type_user_id, _zeitbild.enum_access_level>(
x => x.toFixed(0), x => x.toFixed(0),
{ {
"pairs": ( "pairs": (
dispersal.access_attributed_user_rows dispersal.access_attributed_rows
.map( .map(
(access_attributed_user_row) => ({ (access_attributed_row) => ({
// "calendar_id": access_attributed_user_row["calendar_id"], // "calendar_id": access_attributed_row["calendar_id"],
// "key": access_attributed_user_row["preview"]["user_id"], // "key": access_attributed_row["preview"]["user_id"],
"key": access_attributed_user_row.user_id, "key": access_attributed_row.user_id,
// "value": decode_access_level(access_attributed_user_row["preview"]["level"]), // "value": decode_access_level(access_attributed_row["preview"]["level"]),
"value": decode_access_level(access_attributed_user_row.level), "value": decode_access_level(access_attributed_row.level),
}) })
) )
), ),
@ -328,69 +237,55 @@ namespace _zeitbild.repository.calendar
} }
/** /**
*/ */
export async function read( export function read(
id : _zeitbild.type_calendar_id id : _zeitbild.type_calendar_id
) ) : Promise<_zeitbild.type_calendar_object>
: Promise<_zeitbild.type_calendar_object>
{ {
const core_row : type_core_row = ((await get_core_store().read(id)) as type_core_row); return (
const access_attributed_group_rows : Array<type_access_attributed_group_row> = await ( get_core_store().read(id)
get_access_attributed_group_chest().search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": id,
}
}
)
.then( .then(
(hits) => Promise.resolve<Array<type_access_attributed_group_row>>( (core_row_raw) => {
hits const core_row : type_core_row = (core_row_raw as type_core_row);
.map( return (
hit => ( get_access_attributed_chest().search(
{ {
// "calendar_id": null, "expression": "(calendar_id = $calendar_id)",
"group_id": hit.preview.group_id, "arguments": {
"level": hit.preview.level, "calendar_id": id,
}
} }
) )
) .then(
) (hits) => Promise.resolve<type_dispersal>(
) {
); "core_row": core_row,
const access_attributed_user_rows : Array<type_access_attributed_user_row> = await ( "access_attributed_rows": (
get_access_attributed_user_chest().search( hits
{ .map(
"expression": "(calendar_id = $calendar_id)", hit => (
"arguments": { {
"calendar_id": id, // "calendar_id": null,
} "user_id": hit.preview.user_id,
"level": hit.preview.level,
}
)
)
),
}
)
)
.then(
(dispersal) => Promise.resolve<_zeitbild.type_calendar_object>(
decode(dispersal)
)
)
);
} }
) )
.then(
(hits) => Promise.resolve<Array<type_access_attributed_user_row>>(
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;
} }
@ -405,16 +300,10 @@ namespace _zeitbild.repository.calendar
const calendar_id : _zeitbild.type_calendar_id = await core_store.create( const calendar_id : _zeitbild.type_calendar_id = await core_store.create(
dispersal.core_row dispersal.core_row
); );
for await (const access_attributed_group_row of dispersal.access_attributed_group_rows) { for await (const access_attributed_row of dispersal.access_attributed_rows) {
get_access_attributed_group_chest().write( get_access_attributed_chest().write(
[calendar_id, access_attributed_group_row["group_id"]], [calendar_id, access_attributed_row["user_id"]],
{"level": access_attributed_group_row["level"]} {"level": access_attributed_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); await lib_plankton.cache.clear(_zeitbild.cache_regular);
@ -438,51 +327,10 @@ namespace _zeitbild.repository.calendar
dispersal.core_row dispersal.core_row
); );
} }
// attributed:group // attributed access
{ {
const access_attributed_group_chest = get_access_attributed_group_chest(); const access_attributed_chest = get_access_attributed_chest();
const hits : Array<Record<string, any>> = await access_attributed_group_chest.search( const hits : Array<Record<string, any>> = await access_attributed_chest.search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": calendar_id,
}
}
);
const contrast = lib_plankton.list.contrast<
Record<string, any>,
Record<string, any>
>(
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<Record<string, any>> = await access_attributed_user_chest.search(
{ {
"expression": "(calendar_id = $calendar_id)", "expression": "(calendar_id = $calendar_id)",
"arguments": { "arguments": {
@ -496,25 +344,25 @@ namespace _zeitbild.repository.calendar
>( >(
hits, hits,
hit => hit["user_id"], hit => hit["user_id"],
dispersal.access_attributed_user_rows, dispersal.access_attributed_rows,
row => row["user_id"] row => row["user_id"]
); );
// delete // delete
for await (const entry of contrast.only_left) { for await (const entry of contrast.only_left) {
await access_attributed_user_chest.delete( await access_attributed_chest.delete(
[calendar_id, entry.left["user_id"]] [calendar_id, entry.left["user_id"]]
); );
} }
// update // update
for await (const entry of contrast.both) { for await (const entry of contrast.both) {
await access_attributed_user_chest.write( await access_attributed_chest.write(
[calendar_id, entry.right["user_id"]], [calendar_id, entry.right["user_id"]],
{"level": entry.right["level"]} {"level": entry.right["level"]}
); );
} }
// create // create
for await (const entry of contrast.only_right) { for await (const entry of contrast.only_right) {
await access_attributed_user_chest.write( await access_attributed_chest.write(
[calendar_id, entry.right["user_id"]], [calendar_id, entry.right["user_id"]],
{"level": entry.right["level"]} {"level": entry.right["level"]}
); );
@ -536,16 +384,15 @@ namespace _zeitbild.repository.calendar
{ {
await lib_plankton.cache.clear(_zeitbild.cache_regular); await lib_plankton.cache.clear(_zeitbild.cache_regular);
const core_store = get_core_store(); const core_store = get_core_store();
const access_attributed_user_chest = get_access_attributed_user_chest(); const access_attributed_chest = get_access_attributed_chest();
// attributed:user // attributed access
{ {
const chest = get_access_attributed_user_chest();
const hits : Array< const hits : Array<
{ {
key : Array<any>; key : Array<any>;
preview : Record<string, any>; preview : Record<string, any>;
} }
> = await chest.search( > = await access_attributed_chest.search(
{ {
"expression": "(calendar_id = $calendar_id)", "expression": "(calendar_id = $calendar_id)",
"arguments": { "arguments": {
@ -555,28 +402,9 @@ namespace _zeitbild.repository.calendar
); );
for (const hit of hits) for (const hit of hits)
{ {
await chest.delete(hit.key); await access_attributed_chest.delete(
} hit.key
} );
// attributed:group
{
const chest = get_access_attributed_group_chest();
const hits : Array<
{
key : Array<any>;
preview : Record<string, any>;
}
> = await chest.search(
{
"expression": "(calendar_id = $calendar_id)",
"arguments": {
"calendar_id": calendar_id,
}
}
);
for (const hit of hits)
{
await chest.delete(hit.key);
} }
} }
// core // core
@ -600,162 +428,89 @@ namespace _zeitbild.repository.calendar
/** /**
* @todo caching
*/ */
export async function overview( export async function overview(
user_id : (null | _zeitbild.type_user_id) user_id : (null | _zeitbild.type_user_id)
) ) : Promise<
: Promise<
Array< Array<
type_overview_entry type_overview_entry
> >
> >
{ {
type type_data = {
hits_core : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_group : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_user : Array<
{
key : int;
preview : Record<string, any>;
}
>;
};
return lib_plankton.cache.get_complex<any, Array<type_overview_entry>>( return lib_plankton.cache.get_complex<any, Array<type_overview_entry>>(
_zeitbild.cache_regular, _zeitbild.cache_regular,
"calendar_overview", "calendar_overview",
{ {
"user_id": user_id, "user_id": user_id,
}, },
60, null,
async () => lib_plankton.call.convey( () => (
{ lib_plankton.file.read("sql/calendar_overview.sql")
"hits_core": await get_core_store().search( .then(
(template) => _zeitbild.database.get_implementation().query_free_get(
{ {
"expression": "TRUE", "template": template,
"arguments": {} "arguments": {
"user_id": user_id,
}
} }
), )
"hits_access_attributed_group": await get_access_attributed_group_chest().search( )
(user_id === null) .then(
? (rows) => Promise.resolve(
{ lib_plankton.call.convey(
"expression": "TRUE", rows,
"arguments": {} [
} (x : Array<Record<string, any>>) => x.map(
: (row : Record<string, any>) => ({
{ "id": row["id"],
"expression": "(group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))", "name": row["name"],
"arguments": {"user_id": user_id} "hue": (row["hue"] / hue_scaling),
} /**
), * @todo unite with _zeitbild.service.calendar.get_access_level
"hits_access_attributed_user": await get_access_attributed_user_chest().search( */
(user_id === null) "access_level": decode_access_level(
? Math.max(
{ (row["access_public"] ? 1 : 0),
"expression": "TRUE", (
"arguments": {} (user_id === null)
} ?
: 0
{ :
"expression": "(user_id = $user_id)", (row["access_level_attributed"] ?? row["access_level_default"])
"arguments": {"user_id": user_id} )
} )
), ),
}, })
[ ),
// transform (x : Array<type_overview_entry>) => x.filter(
(data : type_data) => data.hits_core.map( (row) => (
(hit_core) => { ! _zeitbild.value_object.access_level.order(
const calendar_id : _zeitbild.type_calendar_id = hit_core.key; row.access_level,
return { _zeitbild.enum_access_level.none
"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,
[
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hit_access_attributed_group => (
(hit_access_attributed_group.preview.calendar_id === calendar_id)
)
),
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hit_access_attributed_group => hit_access_attributed_group.preview.level
),
(x : Array<int>) => x.map(
decode_access_level
),
]
),
"user": lib_plankton.call.convey(
data.hits_access_attributed_user,
[
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hits_access_attributed_user => (
(hits_access_attributed_user.preview.calendar_id === calendar_id)
)
),
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hits_access_attributed_user => hits_access_attributed_user.preview.level
),
(x : Array<int>) => x.map(
decode_access_level
),
(x : Array<_zeitbild.enum_access_level>) => (x[0] ?? null),
]
),
}
) )
), ),
}; (x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>(
} x,
), {
// only keep visible calendars "compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>(
(x : Array<type_overview_entry>) => x.filter( row => row.access_level,
(row) => ( row => row.id,
! _zeitbild.access_level_order( {
row.access_level, "order_first": (a, b) => _zeitbild.value_object.access_level.order(b, a),
_zeitbild.enum_access_level.none "order_second": (a, b) => (a <= b)
) }
),
}
),
]
) )
), )
// sort by access level and name )
(x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>(
x,
{
"compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>(
row => row.access_level,
row => row.id,
{
"order_first": (a, b) => _zeitbild.access_level_order(b, a),
"order_second": (a, b) => (a <= b)
}
),
}
),
]
) )
); );
} }
} }

View file

@ -1,207 +0,0 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
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<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any>
>
) = null;
/**
*/
function get_store(
)
: lib_plankton.storage.type_store<
_zeitbild.type_user_id,
/*type_row*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any>
>
{
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<string, any>;}> = 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<void>
{
const row : type_row = encode(group_object);
await get_store().update(group_id, row);
return Promise.resolve<void>(undefined);
}
/**
*/
export async function identify(
name : string
)
: Promise<_zeitbild.type_group_id>
{
const hits : Array<{key : _zeitbild.type_group_id; preview : /*type_preview*/Record<string, any>;}> = 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);
}
}
}

View file

@ -0,0 +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
FROM
calendars AS x
LEFT OUTER JOIN calendar_access_attributed AS y ON ((x.id = y.calendar_id) AND (y.user_id = $user_id))
;

View file

@ -23,28 +23,13 @@ namespace _zeitbild.repository.user
/** /**
*/ */
type type_core_row = { type type_row = {
name : string; name : string;
email_address : (null | string); email_address : (null | string);
dav_token : (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 = { type type_preview = {
@ -54,20 +39,12 @@ namespace _zeitbild.repository.user
/** /**
*/ */
type type_dispersal = { var _store : (
core : type_core_row;
groups : Array<type_group_row_slim>;
};
/**
*/
var _store_core : (
null null
| |
lib_plankton.storage.type_store< lib_plankton.storage.type_store<
_zeitbild.type_user_id, _zeitbild.type_user_id,
/*type_core_row*/Record<string, any>, /*type_row*/Record<string, any>,
{}, {},
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any> /*type_preview*/Record<string, any>
@ -77,34 +54,19 @@ namespace _zeitbild.repository.user
/** /**
*/ */
var _store_groups : ( function get_store(
null
|
lib_plankton.storage.type_store<
int,
/*type_group_row_fat*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
) = null;
/**
*/
function get_store_core(
) )
: lib_plankton.storage.type_store< : lib_plankton.storage.type_store<
_zeitbild.type_user_id, _zeitbild.type_user_id,
/*type_core_row*/Record<string, any>, /*type_row*/Record<string, any>,
{}, {},
lib_plankton.storage.type_sql_table_autokey_search_term, lib_plankton.storage.type_sql_table_autokey_search_term,
/*type_preview*/Record<string, any> /*type_preview*/Record<string, any>
> >
{ {
if (_store_core === null) if (_store === null)
{ {
_store_core = lib_plankton.storage.sql_table_autokey_store( _store = lib_plankton.storage.sql_table_autokey_store(
{ {
"database_implementation": _zeitbild.database.get_implementation(), "database_implementation": _zeitbild.database.get_implementation(),
"table_name": "users", "table_name": "users",
@ -115,38 +77,8 @@ namespace _zeitbild.repository.user
else else
{ {
// do nothing // do nothing
} }
return _store_core; return _store;
}
/**
*/
function get_store_groups(
)
: lib_plankton.storage.type_store<
int,
/*type_group_row_fat*/Record<string, any>,
{},
lib_plankton.storage.type_sql_table_autokey_search_term,
Record<string, any>
>
{
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;
} }
@ -155,23 +87,12 @@ namespace _zeitbild.repository.user
function encode( function encode(
user_object : _zeitbild.type_user_object user_object : _zeitbild.type_user_object
) )
: type_dispersal : type_row
{ {
return { return {
"core": { "name": user_object.name,
"name": user_object.name, "email_address": user_object.email_address,
"email_address": user_object.email_address, "dav_token": user_object.dav_token,
"dav_token": user_object.dav_token,
},
"groups": (
user_object.groups.map(
group_id => (
{
"group_id": group_id,
}
)
)
),
}; };
} }
@ -179,19 +100,14 @@ namespace _zeitbild.repository.user
/** /**
*/ */
function decode( function decode(
dispersal : type_dispersal row : type_row
) )
: _zeitbild.type_user_object : _zeitbild.type_user_object
{ {
return { return {
"name": dispersal.core.name, "name": row.name,
"groups": ( "email_address": row.email_address,
dispersal.groups.map( "dav_token": row.dav_token,
group_row => group_row.group_id,
)
),
"email_address": dispersal.core.email_address,
"dav_token": dispersal.core.dav_token,
}; };
} }
@ -209,12 +125,7 @@ namespace _zeitbild.repository.user
> >
> >
{ {
const hits : Array<{key : int; preview : /*type_preview*/Record<string, any>;}> = await get_store_core().search( const hits : Array<{key : int; preview : /*type_preview*/Record<string, any>;}> = await get_store().search({"expression": "TRUE", "arguments": {}});
{
"expression": "TRUE",
"arguments": {}
}
);
return Promise.resolve( return Promise.resolve(
hits hits
.map( .map(
@ -234,40 +145,8 @@ namespace _zeitbild.repository.user
) )
: Promise<_zeitbild.type_user_object> : Promise<_zeitbild.type_user_object>
{ {
const core_row : type_core_row = ((await get_store_core().read(user_id)) as type_core_row); const row : type_row = ((await get_store().read(user_id)) as type_row);
const group_rows : Array<type_group_row_fat> = ( const user_object : _zeitbild.type_user_object = decode(row);
(
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); return Promise.resolve<_zeitbild.type_user_object>(user_object);
} }
@ -279,22 +158,8 @@ namespace _zeitbild.repository.user
) )
: Promise<_zeitbild.type_user_id> : Promise<_zeitbild.type_user_id>
{ {
const dispersal : type_dispersal = encode(user_object); const row : type_row = encode(user_object);
// core const user_id : _zeitbild.type_user_id = await get_store().create(row);
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); return Promise.resolve<_zeitbild.type_user_id>(user_id);
} }
@ -307,57 +172,8 @@ namespace _zeitbild.repository.user
) )
: Promise<void> : Promise<void>
{ {
const dispersal : type_dispersal = encode(user_object); const row : type_row = encode(user_object);
// core await get_store().update(user_id, row);
{
await get_store_core().update(user_id, dispersal.core);
}
// groups
{
const hits : Array<{key : int; preview : Record<string, any>;}> = 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<void>(undefined); return Promise.resolve<void>(undefined);
} }
@ -369,7 +185,7 @@ namespace _zeitbild.repository.user
) )
: Promise<_zeitbild.type_user_id> : Promise<_zeitbild.type_user_id>
{ {
const hits : Array<{key : _zeitbild.type_user_id; preview : /*type_preview*/Record<string, any>;}> = await get_store_core().search( const hits : Array<{key : _zeitbild.type_user_id; preview : /*type_preview*/Record<string, any>;}> = await get_store().search(
{ {
"expression": "(name = $name)", "expression": "(name = $name)",
"arguments": { "arguments": {

View file

@ -91,20 +91,12 @@ namespace _zeitbild.sample
/** /**
*/ */
type type_data = { type type_data = {
groups : Array<
{
id : int;
name : string;
label ?: string;
}
>;
users : Array< users : Array<
{ {
id : int; id : int;
name : string; name : string;
groups ?: Array<int>;
email_address : string; email_address : string;
dav_token ?: (null | string); dav_token : (null | string);
password : string; password : string;
} }
>; >;
@ -115,13 +107,7 @@ namespace _zeitbild.sample
access : { access : {
public ?: boolean; public ?: boolean;
default_level : ("none" | "view" | "edit" | "admin"); default_level : ("none" | "view" | "edit" | "admin");
attributed_group ?: Array< attributed : Array<
{
group_id : int;
level : ("none" | "view" | "edit" | "admin");
}
>;
attributed_user ?: Array<
{ {
user_id : int; user_id : int;
level : ("none" | "view" | "edit" | "admin"); level : ("none" | "view" | "edit" | "admin");
@ -236,10 +222,6 @@ namespace _zeitbild.sample
) : Promise<void> ) : Promise<void>
{ {
let track : { let track : {
group : Record<
int,
_zeitbild.type_group_id
>;
user : Record< user : Record<
int, int,
_zeitbild.type_user_id _zeitbild.type_user_id
@ -249,153 +231,115 @@ namespace _zeitbild.sample
_zeitbild.type_user_id _zeitbild.type_user_id
>; >;
} = { } = {
"group": {},
"user": {}, "user": {},
"calendar": {}, "calendar": {},
}; };
// groups for await (const user_raw of data.users)
{ {
for await (const group_raw of data.groups) const user_object : _zeitbild.type_user_object = {
{ "name": user_raw.name,
const group_object : _zeitbild.type_group_object = { "email_address": user_raw.email_address,
"name": group_raw.name, "dav_token": user_raw.dav_token,
"label": (group_raw.label ?? group_raw.name), };
}; const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add( user_object
group_object );
); await _zeitbild.service.auth_internal.set(
track.group[group_raw.id] = group_id; user_raw.name,
} user_raw.password
);
track.user[user_raw.id] = user_id;
} }
// users for await (const calendar_raw of data.calendars)
{ {
for await (const user_raw of data.users) let resource_object : _zeitbild.type_resource_object;
let resource_id : _zeitbild.type_resource_id;
switch (calendar_raw.resource.kind)
{ {
const user_object : _zeitbild.type_user_object = { case "local":
"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",
resource_object = { "data": {
"kind": "local", "event_ids": [],
"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);
} }
}; )
resource_id = await _zeitbild.service.resource.add( );
resource_object break;
);
/*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 = case "ics_feed":
{ {
"name": calendar_raw.name, resource_object = {
"hue": ( "kind": "ics_feed",
calendar_raw.hue "data": {
?? "url": calendar_raw.resource.data.url,
((calendar_raw.id * phi) % 1) "from_fucked_up_wordpress": (calendar_raw.resource.data.from_fucked_up_wordpress ?? false),
), }
"access": { };
"public": (calendar_raw.access.public ?? false), resource_id = await _zeitbild.service.resource.add(
"default_level": _zeitbild.access_level_from_string(calendar_raw.access.default_level), resource_object
"attributed_group": lib_plankton.map.hashmap.implementation_map( );
lib_plankton.map.hashmap.make( break;
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;
} }
const calendar_object : _zeitbild.type_calendar_object =
{
"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),
})
)
),
}
)
),
},
"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<void>(undefined); return Promise.resolve<void>(undefined);
} }

View file

@ -23,24 +23,41 @@ namespace _zeitbild.service.calendar
/** /**
*/ */
async function get_access_level( function get_access_level(
calendar_object : _zeitbild.type_calendar_object, calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id) user_id : (null | _zeitbild.type_user_id)
) ) : _zeitbild.enum_access_level
: Promise<_zeitbild.enum_access_level>
{ {
return _zeitbild.access_level_determine( return (
calendar_object, lib_plankton.list.max<_zeitbild.enum_access_level, _zeitbild.enum_access_level>(
( [
(user_id === null) (
? calendar_object.access.public
null ?
: _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,
{ {
"id": user_id, "compare_value": _zeitbild.value_object.access_level.order,
"object": (await _zeitbild.service.user.get(user_id)),
} }
) )?.value
??
_zeitbild.enum_access_level.none
); );
} }
@ -48,7 +65,7 @@ namespace _zeitbild.service.calendar
/** /**
* checks if a user has a sufficient access level * checks if a user has a sufficient access level
*/ */
async function wrap_check_access_level<type_result>( function wrap_check_access_level<type_result>(
calendar_object : _zeitbild.type_calendar_object, calendar_object : _zeitbild.type_calendar_object,
user_id : (null | _zeitbild.type_user_id), user_id : (null | _zeitbild.type_user_id),
threshold : _zeitbild.enum_access_level, threshold : _zeitbild.enum_access_level,
@ -57,29 +74,26 @@ namespace _zeitbild.service.calendar
=> =>
Promise<type_result> Promise<type_result>
) )
) ) : Promise<type_result>
: Promise<type_result>
{ {
const access_level : _zeitbild.enum_access_level = await get_access_level( const access_level : _zeitbild.enum_access_level = get_access_level(
calendar_object, calendar_object,
user_id user_id
); );
if (! _zeitbild.access_level_order(threshold, access_level)) if (! _zeitbild.value_object.access_level.order(threshold, access_level)) {
{
return Promise.reject<type_result>( return Promise.reject<type_result>(
new Error( new Error(
lib_plankton.string.coin( lib_plankton.string.coin(
"insufficient access level; at least required: {{threshold}}, actual: {{actual}}", "insufficient access level; at least required: {{threshold}}, actual: {{actual}}",
{ {
"threshold": _zeitbild.access_level_to_string(threshold), "threshold": _zeitbild.value_object.access_level.to_string(threshold),
"actual": _zeitbild.access_level_to_string(access_level), "actual": _zeitbild.value_object.access_level.to_string(access_level),
} }
) )
) )
); );
} }
else else {
{
return success_handler(access_level); return success_handler(access_level);
} }
} }
@ -565,7 +579,7 @@ namespace _zeitbild.service.calendar
const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read( const calendar_object : _zeitbild.type_calendar_object = await _zeitbild.repository.calendar.read(
calendar_id calendar_id
); );
const access_level : _zeitbild.enum_access_level = await get_access_level( const access_level : _zeitbild.enum_access_level = get_access_level(
calendar_object, calendar_object,
user_id user_id
); );

View file

@ -1,63 +0,0 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
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<void>
{
return _zeitbild.repository.group.update(group_id, group_object);
}
}

View file

@ -33,19 +33,6 @@ namespace _zeitbild
} }
/**
*/
export type type_group_id = int;
/**
*/
export type type_group_object = {
name : string;
label : string;
};
/** /**
*/ */
export type type_user_id = int; export type type_user_id = int;
@ -55,9 +42,6 @@ namespace _zeitbild
*/ */
export type type_user_object = { export type type_user_object = {
name : string; name : string;
groups : Array<
type_group_id
>;
email_address : ( email_address : (
null null
| |
@ -147,11 +131,7 @@ namespace _zeitbild
access : { access : {
public : boolean; public : boolean;
default_level : enum_access_level; default_level : enum_access_level;
attributed_group : lib_plankton.map.type_map< attributed : lib_plankton.map.type_map<
type_group_id,
enum_access_level
>;
attributed_user : lib_plankton.map.type_map<
type_user_id, type_user_id,
enum_access_level enum_access_level
>; >;

View file

@ -0,0 +1,74 @@
/*
This file is part of »zeitbild«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
/**
*/
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));
}
}

View file

@ -18,7 +18,7 @@ cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc
## rules ## rules
.PHONY: default .PHONY: default
default: node_modules ${dir_build}/zeitbild node_modules default: node_modules sql ${dir_build}/zeitbild node_modules
.PHONY: node_modules .PHONY: node_modules
node_modules: node_modules:
@ -26,6 +26,13 @@ node_modules:
@ ${cmd_log} "node modules …" @ ${cmd_log} "node modules …"
@ ${cmd_cp} -r -u ${dir_lib}/node/node_modules/* ${dir_build}/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_temp}/conf.ts: \
${dir_source}/conf.ts.tpl \ ${dir_source}/conf.ts.tpl \
${dir_source}/conf.schema.json ${dir_source}/conf.schema.json
@ -40,14 +47,12 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/database.ts \ ${dir_source}/database.ts \
${dir_source}/auth.ts \ ${dir_source}/auth.ts \
${dir_source}/types.ts \ ${dir_source}/types.ts \
${dir_source}/logic.ts \ ${dir_source}/value_objects/access_level.ts \
${dir_source}/repositories/auth_internal.ts \ ${dir_source}/repositories/auth_internal.ts \
${dir_source}/repositories/group.ts \
${dir_source}/repositories/user.ts \ ${dir_source}/repositories/user.ts \
${dir_source}/repositories/resource.ts \ ${dir_source}/repositories/resource.ts \
${dir_source}/repositories/calendar.ts \ ${dir_source}/repositories/calendar.ts \
${dir_source}/services/auth_internal.ts \ ${dir_source}/services/auth_internal.ts \
${dir_source}/services/group.ts \
${dir_source}/services/user.ts \ ${dir_source}/services/user.ts \
${dir_source}/services/resource.ts \ ${dir_source}/services/resource.ts \
${dir_source}/services/calendar.ts \ ${dir_source}/services/calendar.ts \
@ -60,7 +65,6 @@ ${dir_temp}/zeitbild-unlinked.js: \
${dir_source}/api/actions/session_oidc.ts \ ${dir_source}/api/actions/session_oidc.ts \
${dir_source}/api/actions/session_end.ts \ ${dir_source}/api/actions/session_end.ts \
${dir_source}/api/actions/session_status.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/users.ts \
${dir_source}/api/actions/user_dav_conf.ts \ ${dir_source}/api/actions/user_dav_conf.ts \
${dir_source}/api/actions/user_dav_token.ts \ ${dir_source}/api/actions/user_dav_token.ts \