diff --git a/README.md b/README.md index a10c9f315..a18dba6c6 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,13 @@ delete button. Clicking this button will retract consent for only that service. In order for this feature to work, you need to have an EngineBlock instance that supports this feature. See the EngineBlock docs for more information on enabling the feature on the EngineBlock Api. +## OpenConext Invite roles + +By setting the `invite_roles_enabled` flag to `true`, Profile will display the Invite Roles assigned to the logged in +user. By default the roles page is disabled. + +See: https://github.com/OpenConext/OpenConext-Invite + ## Release strategy Please read: https://github.com/OpenConext/Stepup-Deploy/wiki/Release-Management for more information on the release strategy used in Openconext projects. diff --git a/assets/css/helpers/variables.scss b/assets/css/helpers/variables.scss index e4f8c04a5..03a33dbe6 100644 --- a/assets/css/helpers/variables.scss +++ b/assets/css/helpers/variables.scss @@ -97,3 +97,5 @@ $myServices_active: "data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns= $connections: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='33' viewBox='0 0 32 33' style='fill:none;stroke:%23000;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;'%3E%3Ctitle%3Ecog%3C/title%3E%3Cpath d='M10.546,2.438a1.957,1.957,0,0,0,2.908,0L14.4,1.4a1.959,1.959,0,0,1,3.41,1.413l-.071,1.4A1.958,1.958,0,0,0,19.79,6.267l1.4-.071A1.959,1.959,0,0,1,22.6,9.606l-1.042.94a1.96,1.96,0,0,0,0,2.909l1.042.94a1.959,1.959,0,0,1-1.413,3.41l-1.4-.071a1.958,1.958,0,0,0-2.056,2.056l.071,1.4A1.959,1.959,0,0,1,14.4,22.6l-.941-1.041a1.959,1.959,0,0,0-2.908,0L9.606,22.6A1.959,1.959,0,0,1,6.2,21.192l.072-1.4a1.958,1.958,0,0,0-2.056-2.056l-1.4.071A1.958,1.958,0,0,1,1.4,14.4l1.041-.94a1.96,1.96,0,0,0,0-2.909L1.4,9.606A1.958,1.958,0,0,1,2.809,6.2l1.4.071A1.958,1.958,0,0,0,6.267,4.211L6.2,2.81A1.959,1.959,0,0,1,9.606,1.4Z'/%3E%3Ccircle cx='12' cy='12.001' r='4.5'/%3E%3C/svg%3E%0A"; $connections_active: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='33' viewBox='0 0 32 33' style='fill:none;stroke:%23008738;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;'%3E%3Ctitle%3Ecog%3C/title%3E%3Cpath d='M10.546,2.438a1.957,1.957,0,0,0,2.908,0L14.4,1.4a1.959,1.959,0,0,1,3.41,1.413l-.071,1.4A1.958,1.958,0,0,0,19.79,6.267l1.4-.071A1.959,1.959,0,0,1,22.6,9.606l-1.042.94a1.96,1.96,0,0,0,0,2.909l1.042.94a1.959,1.959,0,0,1-1.413,3.41l-1.4-.071a1.958,1.958,0,0,0-2.056,2.056l.071,1.4A1.959,1.959,0,0,1,14.4,22.6l-.941-1.041a1.959,1.959,0,0,0-2.908,0L9.606,22.6A1.959,1.959,0,0,1,6.2,21.192l.072-1.4a1.958,1.958,0,0,0-2.056-2.056l-1.4.071A1.958,1.958,0,0,1,1.4,14.4l1.041-.94a1.96,1.96,0,0,0,0-2.909L1.4,9.606A1.958,1.958,0,0,1,2.809,6.2l1.4.071A1.958,1.958,0,0,0,6.267,4.211L6.2,2.81A1.959,1.959,0,0,1,9.606,1.4Z'/%3E%3Ccircle cx='12' cy='12.001' r='4.5'/%3E%3C/svg%3E%0A"; $conextLogo: "data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 202.6 62.5' style='enable-background:new 0 0 202.6 62.5;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E %23SURFconext %7B fill: %23fff;%7D%0A%3C/style%3E%3Cg id='SURFconext'%3E%3Cpath d='M116.2,13c0.2,0.3,0.4,0.7,0.4,1c0,0.9-0.8,1.6-1.7,1.6c-0.5,0-1-0.2-1.3-0.6c-1-1-2.2-1.6-3.5-1.6c-3.1,0-5.3,2.4-5.3,5.6 s2.2,5.6,5.3,5.6c1.3,0,2.5-0.6,3.5-1.6c0.4-0.4,0.9-0.6,1.4-0.6c0.9,0,1.7,0.7,1.7,1.6c0,0.4-0.1,0.7-0.4,1 c-1.3,1.5-3.2,2.8-6.1,2.8c-5.1,0-9-3.6-9-8.8s3.9-8.8,9-8.8C112.9,10.2,114.9,11.4,116.2,13z'/%3E%3Cpath d='M127.3,27.7c-5.1,0-8.8-3.7-8.8-8.8s3.7-8.8,8.8-8.8c5.1,0,8.9,3.7,8.9,8.8S132.4,27.7,127.3,27.7z M127.3,13.4 c-3.1,0-5.1,2.4-5.1,5.6c0,3.2,2,5.6,5.1,5.6s5.2-2.4,5.2-5.6C132.5,15.7,130.4,13.4,127.3,13.4z'/%3E%3Cpath d='M151.6,27.5c-0.7,0-1.4-0.3-1.8-0.9l-7.5-10.3v9.4c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8V12.6c0-1.2,1-2.3,2.4-2.3 c0.8,0,1.5,0.4,2,1l7.2,9.8v-9c0-1,0.8-1.8,1.8-1.8c1,0,1.8,0.8,1.8,1.8v13.3C153.9,26.5,152.9,27.5,151.6,27.5z'/%3E%3Cpath d='M168.8,27.4h-8.6c-1.1,0-2-0.9-2-2V12.5c0-1.1,0.9-2,2-2h8.6c0.8,0,1.5,0.7,1.5,1.6c0,0.9-0.7,1.6-1.5,1.6h-7v3.6h6.8 c0.8,0,1.6,0.7,1.6,1.6c0,0.9-0.7,1.6-1.6,1.6h-6.8v3.8h7c0.8,0,1.5,0.7,1.5,1.5C170.4,26.7,169.7,27.4,168.8,27.4z'/%3E%3Cpath d='M185.1,27.5c-0.6,0-1.1-0.3-1.4-0.8l-3.8-5.6l-3.9,5.6c-0.3,0.5-0.9,0.7-1.4,0.7c-0.9,0-1.8-0.7-1.8-1.7 c0-0.4,0.1-0.7,0.3-1l4.3-6.1l-4-5.7c-0.2-0.3-0.3-0.6-0.3-0.9c0-0.9,0.7-1.7,1.8-1.7c0.6,0,1.1,0.3,1.5,0.8l3.5,5.1l3.4-5.1 c0.3-0.5,0.9-0.8,1.5-0.8c1,0,1.8,0.8,1.8,1.7c0,0.3-0.1,0.7-0.3,1l-4,5.7l4.3,6.1c0.2,0.3,0.3,0.6,0.3,1 C186.9,26.6,186.2,27.5,185.1,27.5z'/%3E%3Cpath d='M201.1,13.6h-3.5v12.1c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8V13.6h-3.5c-0.9,0-1.6-0.7-1.6-1.6c0-0.8,0.7-1.5,1.6-1.5 h10.7c0.8,0,1.5,0.7,1.5,1.6C202.6,12.9,201.9,13.6,201.1,13.6z'/%3E%3Cg id='surf_5_'%3E%3Cpath class='st0' d='M113.2,37.7c5.2,0,9.4,4.2,9.4,9.4v6c0,5.2-4.2,9.4-9.4,9.4h-14c-5.2,0-9.4-4.2-9.4-9.4v-3.7 c0-6.4-5.2-11.7-11.7-11.7H11.7C5.2,37.7,0,32.5,0,26.1V11.7C0,5.2,5.2,0,11.7,0h66.5c6.4,0,11.7,5.2,11.7,11.7v14.4 c0,6.4,5.2,11.7,11.7,11.7H113.2z M17.4,24c-1.5,0-2.5-0.3-3.4-0.6c-0.6-0.2-1.1-0.4-1.7-0.4c-1.1,0-1.8,0.7-1.8,1.8 c0,1.8,3.7,2.8,6.9,2.8c3.8,0,6.7-2,6.7-5.2c0-2.9-2-4.2-4.1-4.9l-3.2-1c-1.3-0.4-1.9-0.8-1.9-1.5c0-0.9,1.3-1.3,2.4-1.3 c1.3,0,2.3,0.3,3.1,0.6c0.5,0.2,1,0.4,1.5,0.4c1,0,1.6-0.7,1.6-1.8c0-1.8-3.4-2.8-6.2-2.8c-3.7,0-6.4,2.1-6.4,5.1 c0,2.5,1.8,3.9,3.8,4.6l2.8,0.9c1.4,0.4,2.5,0.8,2.5,1.7C20.1,23.4,18.6,24,17.4,24z M39.3,19.6c0,2.6-1.6,4.2-3.8,4.2 c-2.2,0-3.8-1.6-3.8-4.2v-7.4c0-1.3-0.7-1.9-2-1.9c-1.3,0-1.9,0.7-1.9,1.9v7.4c0,4.9,3.3,8,7.7,8c4.4,0,7.7-3.1,7.7-8v-7.4 c0-1.3-0.7-1.9-2-1.9c-1.3,0-1.9,0.7-1.9,1.9V19.6z M57.1,26.3c0.4,0.8,0.9,1.2,1.6,1.2c1,0,2.2-0.7,2.2-1.8c0-0.3-0.1-0.7-0.2-1 L58.9,21c1.7-0.9,2.6-2.5,2.6-4.7c0-3.5-2.6-6-6.3-6h-5.6c-1.3,0-1.9,0.7-1.9,1.9v13.2c0,1.3,0.7,1.9,1.9,1.9c1.3,0,2-0.7,2-1.9 v-3.4h3.5L57.1,26.3z M54.9,14.1c1.5,0,2.6,0.7,2.6,2.3c0,1.6-1.2,2.2-2.6,2.2h-3.3v-4.5H54.9z M74.2,21.4c1.2,0,1.8-0.6,1.8-1.8 c0-1.2-0.6-1.9-1.8-1.9h-4.6v-3.6H77c1.2,0,1.8-0.6,1.8-1.9c0-1.2-0.6-1.8-1.8-1.8h-9.3c-1.3,0-1.9,0.7-1.9,1.9v13.2 c0,1.3,0.7,1.9,1.9,1.9c1.3,0,1.9-0.7,1.9-1.9v-4.2H74.2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A"; +$invite_roles: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 16 16' fill='currentColor' id='Users-Three-Light--Streamline-Phosphor.svg' height='32' width='32'%3E%3Cdesc%3EUsers Three Light Streamline Icon: https://streamlinehq.com%3C/desc%3E%3Cpath d='M14.700509765625 8.291642578125002c-0.16512890625 0.12384375 -0.39938085937499995 0.090380859375 -0.523224609375 -0.0747421875 -0.632478515625 -0.8502421875 -1.631162109375 -1.3495839843750002 -2.69084765625 -1.345423828125 -0.2876953125 0 -0.467501953125 -0.311443359375 -0.32365429687499997 -0.5605957031250001 0.066755859375 -0.11562890625 0.19013671875000002 -0.186861328125 0.32365429687499997 -0.186861328125 1.2466875000000002 -0.00027539062500000003 2.02557421875 -1.3500234375000002 1.40199609375 -2.42954296875 -0.623578125 -1.079525390625 -2.1819375 -1.079185546875 -2.805041015625 0.000609375 -0.0731484375 0.126767578125 -0.12877734375 0.26285742187500005 -0.165369140625 0.40456640625 -0.071923828125 0.27858984375 -0.41845312500000004 0.374841796875 -0.62375390625 0.17326171875 -0.095279296875 -0.09355078125 -0.133412109375 -0.23083007812499998 -0.10003125 -0.36012304687499996 0.45373242187499996 -1.7646796875 2.6476347656250003 -2.376427734375 3.9490253906249997 -1.101140625 1.028783203125 1.008140625 0.926150390625 2.693736328125 -0.21734765625 3.569619140625 0.7369687500000001 0.275009765625 1.3790449218749998 0.75662109375 1.8493359375 1.387154296875 0.12384375 0.16512304687499998 0.090380859375 0.399375 -0.0747421875 0.5232187500000001Zm-3.38909765625 3.9989003906250002c0.1034296875 0.178705078125 0.042345703125 0.407419921875 -0.136412109375 0.51076171875 -0.056660156249999996 0.033210937499999996 -0.12118945312499999 0.050630859375 -0.1868671875 0.05044921875 -0.13360546874999998 0.000087890625 -0.257091796875 -0.07115624999999999 -0.32389453125 -0.186861328125 -1.422064453125 -2.409380859375 -4.907654296875 -2.409380859375 -6.32971875 0 -0.133353515625 0.254923828125 -0.49266210937499993 0.269888671875 -0.6467578125 0.02694140625 -0.077578125 -0.122314453125 -0.07749609375 -0.2784375 0.000205078125 -0.400669921875 0.509044921875 -0.8746171875 1.306072265625 -1.545263671875 2.2548339843750003 -1.897294921875 -1.849939453125 -1.2011367187500002 -1.70588671875 -3.95445703125 0.25929492187500003 -4.95598828125 1.9651816406249998 -1.0015253906250001 4.2776015625 0.49988671874999996 4.162359374999999 2.702548828125 -0.0479296875 0.916107421875 -0.531615234375 1.753875 -1.301021484375 2.253439453125 0.9461191406249999 0.35323242187500004 1.7405390625000001 1.02350390625 2.247978515625 1.896673828125ZM7.5 10.110457031249998c1.6302832031249999 0 2.649205078125 -1.76483203125 1.83406640625 -3.1766953125 -0.815138671875 -1.41186328125 -2.85298828125 -1.41186328125 -3.6681328125 0 -0.18587109375 0.32194921875 -0.283728515625 0.6871464843749999 -0.283728515625 1.0589003906249999 0 1.1696250000000001 0.9481699218749999 2.1177949218750003 2.1177949218750003 2.1177949218750003Zm-3.612708984375 -3.612708984375c0 -0.20640234375000002 -0.16733203124999999 -0.37372265625 -0.37372851562500004 -0.37372851562500004 -1.2466875000000002 -0.0003984375 -2.025439453125 -1.350216796875 -1.4017558593750001 -2.42968359375 0.6236835937499999 -1.0794609375 2.1820429687500003 -1.07896875 2.805041015625 0.000890625 0.073001953125 0.12653906250000002 0.12854882812500001 0.26237109375 0.16512890625 0.40380468750000004 0.071923828125 0.278583984375 0.41845312500000004 0.374841796875 0.62375390625 0.17326171875 0.095279296875 -0.093556640625 0.133412109375 -0.2308359375 0.10003125 -0.36012890625000005C5.352029296875 2.147484375 3.158126953125 1.5357421875000001 1.856736328125 2.8110234375000003 0.827953125 3.819169921875 0.9305859375 5.504765624999999 2.074083984375 6.380642578125c-0.737044921875 0.275185546875 -1.3791269531249999 0.757013671875 -1.8493359375 1.38778125 -0.17261718750000002 0.23015625 -0.03134765625 0.56087109375 0.254279296875 0.5952832031249999 0.1325625 0.01597265625 0.263578125 -0.039990234375 0.3436875 -0.146806640625 0.632484375 -0.8502421875 1.631162109375 -1.3495839843750002 2.69084765625 -1.345423828125 0.206396484375 -0.00001171875 0.37372851562500004 -0.16733203124999999 0.37372851562500004 -0.37372851562500004Z' stroke-width='1'%3E%3C/path%3E%3C/svg%3E"; +$invite_roles_active: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 16 16' fill='currentColor' style='fill: %23008738;' id='Users-Three-Light--Streamline-Phosphor.svg' height='32' width='32'%3E%3Cdesc%3EUsers Three Light Streamline Icon: https://streamlinehq.com%3C/desc%3E%3Cpath d='M14.700509765625 8.291642578125002c-0.16512890625 0.12384375 -0.39938085937499995 0.090380859375 -0.523224609375 -0.0747421875 -0.632478515625 -0.8502421875 -1.631162109375 -1.3495839843750002 -2.69084765625 -1.345423828125 -0.2876953125 0 -0.467501953125 -0.311443359375 -0.32365429687499997 -0.5605957031250001 0.066755859375 -0.11562890625 0.19013671875000002 -0.186861328125 0.32365429687499997 -0.186861328125 1.2466875000000002 -0.00027539062500000003 2.02557421875 -1.3500234375000002 1.40199609375 -2.42954296875 -0.623578125 -1.079525390625 -2.1819375 -1.079185546875 -2.805041015625 0.000609375 -0.0731484375 0.126767578125 -0.12877734375 0.26285742187500005 -0.165369140625 0.40456640625 -0.071923828125 0.27858984375 -0.41845312500000004 0.374841796875 -0.62375390625 0.17326171875 -0.095279296875 -0.09355078125 -0.133412109375 -0.23083007812499998 -0.10003125 -0.36012304687499996 0.45373242187499996 -1.7646796875 2.6476347656250003 -2.376427734375 3.9490253906249997 -1.101140625 1.028783203125 1.008140625 0.926150390625 2.693736328125 -0.21734765625 3.569619140625 0.7369687500000001 0.275009765625 1.3790449218749998 0.75662109375 1.8493359375 1.387154296875 0.12384375 0.16512304687499998 0.090380859375 0.399375 -0.0747421875 0.5232187500000001Zm-3.38909765625 3.9989003906250002c0.1034296875 0.178705078125 0.042345703125 0.407419921875 -0.136412109375 0.51076171875 -0.056660156249999996 0.033210937499999996 -0.12118945312499999 0.050630859375 -0.1868671875 0.05044921875 -0.13360546874999998 0.000087890625 -0.257091796875 -0.07115624999999999 -0.32389453125 -0.186861328125 -1.422064453125 -2.409380859375 -4.907654296875 -2.409380859375 -6.32971875 0 -0.133353515625 0.254923828125 -0.49266210937499993 0.269888671875 -0.6467578125 0.02694140625 -0.077578125 -0.122314453125 -0.07749609375 -0.2784375 0.000205078125 -0.400669921875 0.509044921875 -0.8746171875 1.306072265625 -1.545263671875 2.2548339843750003 -1.897294921875 -1.849939453125 -1.2011367187500002 -1.70588671875 -3.95445703125 0.25929492187500003 -4.95598828125 1.9651816406249998 -1.0015253906250001 4.2776015625 0.49988671874999996 4.162359374999999 2.702548828125 -0.0479296875 0.916107421875 -0.531615234375 1.753875 -1.301021484375 2.253439453125 0.9461191406249999 0.35323242187500004 1.7405390625000001 1.02350390625 2.247978515625 1.896673828125ZM7.5 10.110457031249998c1.6302832031249999 0 2.649205078125 -1.76483203125 1.83406640625 -3.1766953125 -0.815138671875 -1.41186328125 -2.85298828125 -1.41186328125 -3.6681328125 0 -0.18587109375 0.32194921875 -0.283728515625 0.6871464843749999 -0.283728515625 1.0589003906249999 0 1.1696250000000001 0.9481699218749999 2.1177949218750003 2.1177949218750003 2.1177949218750003Zm-3.612708984375 -3.612708984375c0 -0.20640234375000002 -0.16733203124999999 -0.37372265625 -0.37372851562500004 -0.37372851562500004 -1.2466875000000002 -0.0003984375 -2.025439453125 -1.350216796875 -1.4017558593750001 -2.42968359375 0.6236835937499999 -1.0794609375 2.1820429687500003 -1.07896875 2.805041015625 0.000890625 0.073001953125 0.12653906250000002 0.12854882812500001 0.26237109375 0.16512890625 0.40380468750000004 0.071923828125 0.278583984375 0.41845312500000004 0.374841796875 0.62375390625 0.17326171875 0.095279296875 -0.093556640625 0.133412109375 -0.2308359375 0.10003125 -0.36012890625000005C5.352029296875 2.147484375 3.158126953125 1.5357421875000001 1.856736328125 2.8110234375000003 0.827953125 3.819169921875 0.9305859375 5.504765624999999 2.074083984375 6.380642578125c-0.737044921875 0.275185546875 -1.3791269531249999 0.757013671875 -1.8493359375 1.38778125 -0.17261718750000002 0.23015625 -0.03134765625 0.56087109375 0.254279296875 0.5952832031249999 0.1325625 0.01597265625 0.263578125 -0.039990234375 0.3436875 -0.146806640625 0.632484375 -0.8502421875 1.631162109375 -1.3495839843750002 2.69084765625 -1.345423828125 0.206396484375 -0.00001171875 0.37372851562500004 -0.16733203124999999 0.37372851562500004 -0.37372851562500004Z' stroke-width='1'%3E%3C/path%3E%3C/svg%3E"; diff --git a/assets/css/pages/inviteRoles/inviteRoles.scss b/assets/css/pages/inviteRoles/inviteRoles.scss new file mode 100644 index 000000000..51eb438f3 --- /dev/null +++ b/assets/css/pages/inviteRoles/inviteRoles.scss @@ -0,0 +1,43 @@ +@import '../../helpers/functions'; + +.content__title { + color: $black; +} + +.inviteRoles { + width: 100%; + + &__container { + padding: 1rem; + width: 100%; + background-color: $white; + border-radius: 6px; + border: 1px solid $idpSeparatorGray; + margin-bottom: 1rem; + } + + &__top{ + display: flex; + } + &__bottom{ + display: flex; + height: 3.5rem; + .button { + display:block; + margin-top: 1rem; + } + } + &__logo { + height: 90px; + min-width: 90px; + align-content: flex-start; + margin-right: 1rem; + img { + height: inherit; + } + } + + &__launch { + margin-left: auto; + } +} diff --git a/assets/css/pages/pages.scss b/assets/css/pages/pages.scss index 000bc69b7..effdc845e 100644 --- a/assets/css/pages/pages.scss +++ b/assets/css/pages/pages.scss @@ -1,5 +1,6 @@ @import 'home/home'; @import 'error/error'; +@import 'inviteRoles/inviteRoles'; @import 'myConnections/myConnections'; @import 'myProfile/myProfile'; @import 'myServices/myServices'; diff --git a/assets/css/shared/navigation.scss b/assets/css/shared/navigation.scss index ce44db302..903aece01 100644 --- a/assets/css/shared/navigation.scss +++ b/assets/css/shared/navigation.scss @@ -186,3 +186,11 @@ background-image: url($connections_active); } } + +.navigation__inviteRoles { + background-image: url($invite_roles); + + &.active { + background-image: url($invite_roles_active); + } +} diff --git a/ci/qa/phpstan-baseline.neon b/ci/qa/phpstan-baseline.neon index 12c0e533a..733995c63 100644 --- a/ci/qa/phpstan-baseline.neon +++ b/ci/qa/phpstan-baseline.neon @@ -243,6 +243,51 @@ parameters: count: 1 path: ../../src/OpenConext/EngineBlockApiClient/Value/ConsentListFactory.php + - + message: "#^Call to an undefined method Traversable\\\\:\\:getArrayCopy\\(\\)\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Cannot access offset 'description' on mixed\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Cannot access offset 'name' on mixed\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Cannot access offset mixed on mixed\\.$#" + count: 2 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Method OpenConext\\\\InviteApiClientBundle\\\\Tests\\\\Value\\\\InviteRoleListFactoryTest\\:\\:invalidDataProvider\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Method OpenConext\\\\InviteApiClientBundle\\\\Tests\\\\Value\\\\InviteRoleListFactoryTest\\:\\:validDataProvider\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Parameter \\#1 \\$data of static method OpenConext\\\\InviteApiClientBundle\\\\Value\\\\InviteRoleListFactory\\:\\:createList\\(\\) expects array\\, mixed given\\.$#" + count: 2 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Parameter \\#1 \\$exception of method PHPUnit\\\\Framework\\\\TestCase\\:\\:expectException\\(\\) expects class\\-string\\, string given\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php + + - + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, Closure\\(array\\)\\: OpenConext\\\\Profile\\\\Value\\\\InviteRole given\\.$#" + count: 1 + path: ../../src/OpenConext/InviteApiClientBundle/Value/InviteRoleListFactory.php + - message: "#^Method OpenConext\\\\Profile\\\\Assert\\:\\:keysArePresent\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#" count: 1 @@ -640,6 +685,11 @@ parameters: count: 1 path: ../../src/OpenConext/Profile/Value/Entity.php + - + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(string\\)\\: mixed\\)\\|null, Closure\\(array\\)\\: OpenConext\\\\Profile\\\\Value\\\\Application given\\.$#" + count: 1 + path: ../../src/OpenConext/Profile/Value/InviteRole.php + - message: "#^Class OpenConext\\\\Profile\\\\Value\\\\LocaleSet implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" count: 1 diff --git a/config/openconext/parameters.yaml.dist b/config/openconext/parameters.yaml.dist index 8e0bb731c..490737606 100644 --- a/config/openconext/parameters.yaml.dist +++ b/config/openconext/parameters.yaml.dist @@ -55,6 +55,15 @@ parameters: remove_consent_enabled: false + # Feature flag to display the OpenConext Invite roles assigned to the logged in user + invite_roles_enabled: false + + # The Invite API credentials + invite_api_base_url: "https://invite.dev.openconext.local/api/external/" + invite_api_username: profile + invite_api_password: secret + invite_api_verify_ssl: true + # Session handler override # Change to the following to use the database to store sessions: #session_handler: 'Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler' diff --git a/config/packages/http-client.yaml b/config/packages/http-client.yaml index 0264906cf..3a2d8cd7b 100644 --- a/config/packages/http-client.yaml +++ b/config/packages/http-client.yaml @@ -22,3 +22,9 @@ framework: verify_host: '%attribute_aggregation_api_verify_ssl%' verify_peer: '%attribute_aggregation_api_verify_ssl%' + invite_api_client: + base_uri: '%invite_api_base_url%' + auth_basic: '%invite_api_username%:%invite_api_password%' + verify_host: '%invite_api_verify_ssl%' + verify_peer: '%invite_api_verify_ssl%' + diff --git a/config/packages/open_conext.yaml b/config/packages/open_conext.yaml index d3277d141..b361c7468 100644 --- a/config/packages/open_conext.yaml +++ b/config/packages/open_conext.yaml @@ -24,3 +24,4 @@ open_conext_profile: # The Url where the attribute can be connected connect_url: '%attribute_aggregation_orcid_connect_url%' remove_consent_enabled: '%remove_consent_enabled%' + invite_roles_enabled: '%invite_roles_enabled%' diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 35f85793a..a10bb2e90 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,6 +4,7 @@ twig: globals: global_view_parameters: "@open_conext.profile_bundle.service.global_view_parameters" root_path: "%kernel.project_dir%" + navigation_invite_roles_enabled: "%invite_roles_enabled%" paths: '%kernel.project_dir%/templates': 'OpenConextProfile' diff --git a/config/services.yaml b/config/services.yaml index 45261522a..37aa313e3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -24,6 +24,11 @@ services: $removeConsentEnabled: '%remove_consent_enabled%' tags: [ 'controller.service_arguments' ] + OpenConext\ProfileBundle\Controller\InviteRolesController: + arguments: + $enabled: '%invite_roles_enabled%' + tags: [ 'controller.service_arguments' ] + surfnet_saml.saml_provider: class: OpenConext\ProfileBundle\Security\Authentication\Provider\SamlProvider diff --git a/src/OpenConext/InviteApiClientBundle/Exception/InvalidArgumentException.php b/src/OpenConext/InviteApiClientBundle/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..612c21a77 --- /dev/null +++ b/src/OpenConext/InviteApiClientBundle/Exception/InvalidArgumentException.php @@ -0,0 +1,27 @@ + $parameters + * @return array + */ + public function read( + string $path, + array $parameters = [], + ): array { + $resource = $this->buildResourcePath($path, $parameters); + + $response = $this->inviteApiClient->request('GET', $resource); + + $statusCode = $response->getStatusCode(); + + if ($statusCode === 404) { + throw new ProfileNotFoundException(sprintf('Profile "%s" not found', $resource)); + } + + if ($statusCode !== 200) { + throw new InvalidResponseException( + sprintf( + 'Request to profile "%s" returned an invalid response with status code %s', + $resource, + $statusCode, + ), + ); + } + + try { + $data = $response->toArray(); + } catch (DecodingExceptionInterface) { + throw new MalformedResponseException( + sprintf('Cannot read profile "%s": malformed JSON returned', $resource), + ); + } + + return $data; + } + + /** + * @param array $parameters + */ + private function buildResourcePath( + string $path, + array $parameters, + ): string { + $resource = $path; + if (count($parameters) > 0) { + $resource = $path . '?' . http_build_query($parameters); + } + return $resource; + } +} diff --git a/src/OpenConext/InviteApiClientBundle/OpenConextInviteApiClientBundle.php b/src/OpenConext/InviteApiClientBundle/OpenConextInviteApiClientBundle.php new file mode 100644 index 000000000..0649ea01e --- /dev/null +++ b/src/OpenConext/InviteApiClientBundle/OpenConextInviteApiClientBundle.php @@ -0,0 +1,27 @@ +logger->info(sprintf('OpenConext-invite API: GET v1/profile for "%s', $collabPersonId)); + $inviteRoleList = $this->apiClient->read('v1/profile', ['collabPersonId' => $collabPersonId]); + return InviteRoleListFactory::createList($inviteRoleList); + } +} diff --git a/src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php b/src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php new file mode 100644 index 000000000..7c5082126 --- /dev/null +++ b/src/OpenConext/InviteApiClientBundle/Tests/Value/InviteRoleListFactoryTest.php @@ -0,0 +1,155 @@ +assertInstanceOf(InviteRoleList::class, $result); + $this->assertCount($expectedCount, $result); + + $roles = $result->getIterator()->getArrayCopy(); + $this->assertContainsOnlyInstancesOf(InviteRole::class, $roles); + + foreach ($roles as $index => $role) { + $this->assertEquals($data[$index]['name'], $role->getName()); + $this->assertEquals($data[$index]['description'], $role->getDescription()); + } + } + + public function validDataProvider(): array + { + return [ + 'no api response' => ['[]', 0], + 'response with two roles' => [ + '[ + { + "name": "Admin Role", + "description": "Administrator role with full access", + "applications": [ + { + "landingPage": "https://admin.example.com", + "nameEn": "Admin Panel", + "nameNl": "Beheerderspaneel", + "organisationEn": "Example Org", + "organisationNl": "Voorbeeldorganisatie", + "logo": "https://example.com/logo.png" + } + ] + }, + { + "name": "User Role", + "description": "Standard user role", + "applications": [ + { + "landingPage": "https://user.example.com", + "nameEn": "User Dashboard", + "nameNl": "Gebruikersdashboard", + "organisationEn": "Example Org", + "organisationNl": "Voorbeeldorganisatie", + "logo": "https://example.com/logo.png" + } + ] + } + ]', + 2 + ], + 'without applications section' => [ + '[ + { + "name": "Empty App Role", + "description": "Role with no applications", + "applications": [] + } + ]', + 1 + ], + 'missing applications' => [ + '[ + { + "name": "No App Role", + "description": "Role with missing applications key" + } + ]', + 1 + ], + 'empty strings' => [ + '[ + { + "name": "", + "description": "", + "applications": [] + } + ]', + 1 + ], + 'special characters' => [ + '[ + { + "name": "Special ÇhåráctÐrs !@#$%^&*()", + "description": "Dèscríptîøn with špeçïal çhäræctêrs", + "applications": [] + } + ]', + 1 + ], + ]; + } + + /** + * @dataProvider invalidDataProvider + */ + public function testCreateListWithInvalidData(string $jsonData, string $expectedException): void + { + $this->expectException($expectedException); + + $data = json_decode($jsonData, true); + InviteRoleListFactory::createList($data); + } + + public function invalidDataProvider(): array + { + return [ + 'invalid types' => [ + '[{"name": 123, "description": 456}]', + TypeError::class + ], + 'null values' => [ + '[{"name": null, "description": null}]', + TypeError::class + ] + ]; + } +} diff --git a/src/OpenConext/InviteApiClientBundle/Value/InviteRoleListFactory.php b/src/OpenConext/InviteApiClientBundle/Value/InviteRoleListFactory.php new file mode 100644 index 000000000..cd3795b81 --- /dev/null +++ b/src/OpenConext/InviteApiClientBundle/Value/InviteRoleListFactory.php @@ -0,0 +1,54 @@ + $data + */ + public static function createList( + array $data, + ): InviteRoleList { + $roles = array_map( + self::createInviteRole(...), + $data, + ); + + return new InviteRoleList($roles); + } + + /** + * @param array $data + */ + private static function createInviteRole( + array $data, + ): InviteRole { + $applications = []; + if (array_key_exists('applications', $data)) { + $applications = $data['applications']; + } + return new InviteRole($data['name'], $data['description'], $applications); + } +} diff --git a/src/OpenConext/Profile/Repository/InviteRepositoryInterface.php b/src/OpenConext/Profile/Repository/InviteRepositoryInterface.php new file mode 100644 index 000000000..7c974a540 --- /dev/null +++ b/src/OpenConext/Profile/Repository/InviteRepositoryInterface.php @@ -0,0 +1,30 @@ +nameEn !== '' || $this->nameNl !== ''; + } + + public function hasOrganisationName(): bool + { + return $this->organisationEn !== '' || $this->organisationNl !== ''; + } + + public function hasLogo(): bool + { + return $this->logo !== null; + } + + public function hasLandingPage(): bool + { + return $this->landingPage !== null; + } + + public function getName( + string $locale, + ): string { + switch ($locale) { + case 'nl': + return $this->nameNl; + default: + return $this->nameEn; + } + } + + public function getOrganisationName( + string $locale, + ): string { + switch ($locale) { + case 'nl': + return $this->organisationNl; + default: + return $this->organisationEn; + } + } + + public function getLogo(): string + { + return $this->logo; + } + + public function getLandingPage(): string + { + return $this->landingPage; + } +} diff --git a/src/OpenConext/Profile/Value/InviteRole.php b/src/OpenConext/Profile/Value/InviteRole.php new file mode 100644 index 000000000..9d6dccb10 --- /dev/null +++ b/src/OpenConext/Profile/Value/InviteRole.php @@ -0,0 +1,73 @@ + $applications + */ + public function __construct( + private string $name, + private string $description, + array $applications, + ) { + $this->applications = array_map( + fn(array $appData) => new Application( + $appData['landingPage'] ?? '', + $appData['nameEn'] ?? '', + $appData['nameNl'] ?? '', + $appData['organisationEn'] ?? '', + $appData['organisationNl'] ?? '', + $appData['logo'] ?? '', + ), + $applications, + ); + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): string + { + return $this->description; + } + + public function hasApplications(): bool + { + return count($this->applications) > 0; + } + + /** + * @return array + */ + public function getApplications(): array + { + return $this->applications; + } +} diff --git a/src/OpenConext/Profile/Value/InviteRoleList.php b/src/OpenConext/Profile/Value/InviteRoleList.php new file mode 100644 index 000000000..5d5704dab --- /dev/null +++ b/src/OpenConext/Profile/Value/InviteRoleList.php @@ -0,0 +1,64 @@ + + */ +final class InviteRoleList implements IteratorAggregate, Countable +{ + /** + * @var InviteRole[] + */ + private array $roles = []; + + /** + * @param InviteRole[] $inviteRoles + */ + public function __construct( + array $inviteRoles, + ) { + foreach ($inviteRoles as $invite) { + $this->initializeWith($invite); + } + } + + private function initializeWith( + InviteRole $inviteRole, + ): void { + $this->roles[] = $inviteRole; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->roles); + } + + public function count(): int + { + return count($this->roles); + } +} diff --git a/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php b/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php new file mode 100644 index 000000000..3c1781a33 --- /dev/null +++ b/src/OpenConext/ProfileBundle/Controller/InviteRolesController.php @@ -0,0 +1,58 @@ +enabled) { + throw $this->createAccessDeniedException(); + } + $this->logger->info('Showing the OpenConext-Invite roles page'); + + $user = $this->userProvider->getCurrentUser(); + $inviteRoles = $this->inviteRepository->findAllFor($user->getUserIdentifier()); + return $this->render('@OpenConextProfile/InviteRoles/overview.html.twig', ['inviteRoles' => $inviteRoles]); + } +} diff --git a/src/OpenConext/ProfileBundle/DependencyInjection/Configuration.php b/src/OpenConext/ProfileBundle/DependencyInjection/Configuration.php index 29c98ee08..33fd5ab5b 100644 --- a/src/OpenConext/ProfileBundle/DependencyInjection/Configuration.php +++ b/src/OpenConext/ProfileBundle/DependencyInjection/Configuration.php @@ -46,6 +46,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('This is the feature flag that toggles the remove consent feature') ->isRequired() ->end() + ->booleanNode('invite_roles_enabled') + ->info('This is the feature flag that toggles the OpenConext Invite roles page feature') + ->isRequired() + ->end() ->end(); $this->setupLocaleConfiguration($rootNode); diff --git a/templates/InviteRoles/overview.html.twig b/templates/InviteRoles/overview.html.twig new file mode 100644 index 000000000..a042f23fb --- /dev/null +++ b/templates/InviteRoles/overview.html.twig @@ -0,0 +1,30 @@ +{% extends '@OpenConextProfile/layout.html.twig' %} + +{% block title %} + {{ 'profile.my_profile.short_title'|trans }} — {{ parent() }} +{% endblock %} + +{% block contentClasses %} + content__myProfile +{% endblock %} + +{% block content %} +

{{ 'profile.invite_roles.long_title'|trans }}

+

{{ 'profile.invite_roles.intro'|trans }}

+ + {% if inviteRoles is not empty %} +
+ {% for invite in inviteRoles %} + {% if not invite.hasApplications %} + {% include '@OpenConextProfile/InviteRoles/partial/invite-without-application.html.twig' with {'invite': invite} %} + {% else %} + {% for application in invite.applications %} + {% include '@OpenConextProfile/InviteRoles/partial/invite-with-application.html.twig' with {'invite': invite, 'application': application} %} + {% endfor %} + {% endif %} + {% endfor %} +
+ {% else %} +

{{ 'profile.invite_roles.no_results'|trans }}

+ {% endif %} +{% endblock %} diff --git a/templates/InviteRoles/partial/invite-with-application.html.twig b/templates/InviteRoles/partial/invite-with-application.html.twig new file mode 100644 index 000000000..7dcfaba67 --- /dev/null +++ b/templates/InviteRoles/partial/invite-with-application.html.twig @@ -0,0 +1,24 @@ +{% set locale = app.request.getLocale %} +
+
+ +
+

{{ invite.name }}

+ {% if application.hasName %} +

{{ application.getName(locale) }}{% if application.hasOrganisationName %} ({{ application.organisationName(locale) }}){% endif %}

+ {% endif %} + {{ invite.description }} +
+
+ {% if application.hasLandingPage %} +
+ +
+ {% endif %} +
diff --git a/templates/InviteRoles/partial/invite-without-application.html.twig b/templates/InviteRoles/partial/invite-without-application.html.twig new file mode 100644 index 000000000..fc1441cf8 --- /dev/null +++ b/templates/InviteRoles/partial/invite-without-application.html.twig @@ -0,0 +1,10 @@ +
+
+ +
+

{{ invite.name }}

+ {{ invite.description }} +
+
+
diff --git a/templates/layout/main/navigation.html.twig b/templates/layout/main/navigation.html.twig index a9317a070..e38fd6d95 100644 --- a/templates/layout/main/navigation.html.twig +++ b/templates/layout/main/navigation.html.twig @@ -27,6 +27,14 @@ {{ 'profile.my_profile.short_title'|trans }} + {% if navigation_invite_roles_enabled %} + {% set isActive = route == 'profile.invite_roles' %} + + {% endif %} {% set isActive = route == 'profile.my_surf_conext_overview' %}