From abd63033f76954322648eb4c0411defaf2fda1f1 Mon Sep 17 00:00:00 2001
From: zachmann <gabriel.zachmann@kit.edu>
Date: Fri, 29 Jul 2022 10:24:44 +0200
Subject: [PATCH] improve capability handling

---
 CHANGELOG.md                                  |  8 +++
 .../web/partials/capabilities-part.mustache   |  5 --
 .../partials/capability-single-part.mustache  | 30 +++++++-----
 .../server/web/partials/restrictions.mustache |  1 +
 .../partials/sub-capabilities-part.mustache   |  5 --
 .../sub-capability-single-part.mustache       | 29 ++++++-----
 internal/server/web/static/css/mytoken.css    |  8 +++
 internal/server/web/static/css/restr-gui.css  | 13 +++--
 internal/server/web/static/js/capabilities.js | 49 ++++++++++++++++---
 internal/server/web/static/js/consent.js      | 14 +++++-
 internal/server/web/static/js/create-mt.js    |  6 +--
 11 files changed, 117 insertions(+), 51 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e41f5ab..c51fc0a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,14 @@
 
 - Trusted web applications can skip the consent screen
 
+### Enhancements
+
+- Reworked and improved several parts of the web interface:
+  - Consent Screen: On default a more compressed view is shown, where sections can be expanded if needed.
+  - Capabilities:
+    - Simplified the checking of capabilities
+    - Read/Write capabilities are now not split but can be toggled
+ 
 ### Dependencies
 
 - Bump github.com/valyala/fasthttp from 1.37.0 to 1.38.0
diff --git a/internal/server/web/partials/capabilities-part.mustache b/internal/server/web/partials/capabilities-part.mustache
index 48467087..a2184383 100644
--- a/internal/server/web/partials/capabilities-part.mustache
+++ b/internal/server/web/partials/capabilities-part.mustache
@@ -3,11 +3,6 @@
         {{> capability-single-part}}
     {{/ReadWriteCapability}}
     <ul class="list-group">
-        {{#ReadOnlyCapability}}
-            <li class="list-group-item capability">
-                {{> capability-single-part}}
-            </li>
-        {{/ReadOnlyCapability}}
         {{#Children}}
             {{> capabilities-part}}
         {{/Children}}
diff --git a/internal/server/web/partials/capability-single-part.mustache b/internal/server/web/partials/capability-single-part.mustache
index 4d029b9d..9a8a858b 100644
--- a/internal/server/web/partials/capability-single-part.mustache
+++ b/internal/server/web/partials/capability-single-part.mustache
@@ -6,21 +6,29 @@
         <div class="d-flex justify-content-between">
             <h6>
                 {{Name}}
-                <span class="pl-1" style="font-size: 1rem;">
-                {{^IsReadOnly}}
-                    <i class="fas fa-pencil-alt" data-toggle="tooltip" data-placement="top" title=""
-                       data-original-title="Allows full access."></i>
-                {{/IsReadOnly}}
-                    {{#IsReadOnly}}
-                        <i class="fas fa-book-open" data-toggle="tooltip" data-placement="top" title=""
-                           data-original-title="Allows only read access."></i>
-                    {{/IsReadOnly}}
+                <span class="pl-1" style="font-size: 1rem;" data-toggle="tooltip" data-placement="top" title=""
+                      data-original-title="">
+                    {{#ReadOnlyCapability}}
+                        <input id="cp-{{Name}}-mode" class="rw-cap-mode" type="checkbox" checked data-size="sm"
+                               data-toggle="toggle"
+                               data-onstyle="dark" data-offstyle="dark" data-style="round"
+                               data-on='<i class="fas fa-pencil-alt"></i>'
+                               data-off='<i class="fas fa-book-open"></i>'>
+                    {{/ReadOnlyCapability}}
                 </span>
             </h6>
-            <i class="fas fa-exclamation-circle {{ColorClass}}" data-toggle="tooltip"
+            <i class="fas fa-exclamation-circle {{ColorClass}} rw-cap-write" data-toggle="tooltip"
                data-placement="right" title="" data-original-title="{{CapabilityLevel}}"></i>
+            {{#ReadOnlyCapability}}
+                <i class="fas fa-exclamation-circle {{ColorClass}} rw-cap-read d-none" data-toggle="tooltip"
+                   data-placement="right" title="" data-original-title="{{CapabilityLevel}}"></i>
+            {{/ReadOnlyCapability}}
         </div>
-        <small>{{Description}}</small>
+        <small class="rw-cap-write">{{Description}}</small>
+        {{#ReadOnlyCapability}}
+            <small class="rw-cap-read d-none">{{Description}}</small>
+        {{/ReadOnlyCapability}}
+
         {{#IsCreateMT}}
             <small><br>A created mytoken can have the following
                 capabilities:</small>
diff --git a/internal/server/web/partials/restrictions.mustache b/internal/server/web/partials/restrictions.mustache
index ad0fefb1..8ba96cc0 100644
--- a/internal/server/web/partials/restrictions.mustache
+++ b/internal/server/web/partials/restrictions.mustache
@@ -3,6 +3,7 @@
     <div>
     <span id="restr-editor-wrap" class="{{#collapse}} d-none{{/collapse}}">
         <input id="restr-editor-mode" type="checkbox" checked data-size="sm" data-toggle="toggle"
+               data-style="restr-editor"
                data-onstyle="success" data-offstyle="info" data-on="Easy Editor" data-off="JSON Editor"></span>
         {{#collapse}}
             <button class="btn btn-info my-expand ml-2" type="button" data-toggle="collapse" data-target="#restr-body"
diff --git a/internal/server/web/partials/sub-capabilities-part.mustache b/internal/server/web/partials/sub-capabilities-part.mustache
index a91b71d4..d3c112e8 100644
--- a/internal/server/web/partials/sub-capabilities-part.mustache
+++ b/internal/server/web/partials/sub-capabilities-part.mustache
@@ -3,11 +3,6 @@
         {{> sub-capability-single-part}}
     {{/ReadWriteCapability}}
     <ul class="list-group">
-        {{#ReadOnlyCapability}}
-            <li class="list-group-item capability">
-                {{> sub-capability-single-part}}
-            </li>
-        {{/ReadOnlyCapability}}
         {{#Children}}
             {{> sub-capabilities-part}}
         {{/Children}}
diff --git a/internal/server/web/partials/sub-capability-single-part.mustache b/internal/server/web/partials/sub-capability-single-part.mustache
index 3d445529..34bd9736 100644
--- a/internal/server/web/partials/sub-capability-single-part.mustache
+++ b/internal/server/web/partials/sub-capability-single-part.mustache
@@ -6,20 +6,27 @@
         <div class="d-flex justify-content-between">
             <h6>
                 {{Name}}
-                <span class="pl-1" style="font-size: 1rem;">
-                {{^IsReadOnly}}
-                    <i class="fas fa-pencil-alt" data-toggle="tooltip" data-placement="top" title=""
-                       data-original-title="Allows full access."></i>
-                {{/IsReadOnly}}
-                    {{#IsReadOnly}}
-                        <i class="fas fa-book-open" data-toggle="tooltip" data-placement="top" title=""
-                           data-original-title="Allows only read access. Click to allow full access."></i>
-                    {{/IsReadOnly}}
+                <span class="pl-1" style="font-size: 1rem;" data-toggle="tooltip" data-placement="top" title=""
+                      data-original-title="">
+                    {{#ReadOnlyCapability}}
+                        <input id="sub-cp-{{Name}}-mode" class="rw-cap-mode" type="checkbox" checked data-size="sm"
+                               data-toggle="toggle"
+                               data-onstyle="light" data-offstyle="light" data-style="round"
+                               data-on='<i class="fas fa-pencil-alt"></i>'
+                               data-off='<i class="fas fa-book-open"></i>'>
+                    {{/ReadOnlyCapability}}
                 </span>
             </h6>
-            <i class="fas fa-exclamation-circle {{ColorClass}}" data-toggle="tooltip"
+            <i class="fas fa-exclamation-circle {{ColorClass}} rw-cap-write" data-toggle="tooltip"
                data-placement="right" title="" data-original-title="{{CapabilityLevel}}"></i>
+            {{#ReadOnlyCapability}}
+                <i class="fas fa-exclamation-circle {{ColorClass}} rw-cap-read d-none" data-toggle="tooltip"
+                   data-placement="right" title="" data-original-title="{{CapabilityLevel}}"></i>
+            {{/ReadOnlyCapability}}
         </div>
-        <small>{{Description}}</small>
+        <small class="rw-cap-write">{{Description}}</small>
+        {{#ReadOnlyCapability}}
+            <small class="rw-cap-read d-none">{{Description}}</small>
+        {{/ReadOnlyCapability}}
     </div>
 </div>
diff --git a/internal/server/web/static/css/mytoken.css b/internal/server/web/static/css/mytoken.css
index 02b8df80..4b251f39 100644
--- a/internal/server/web/static/css/mytoken.css
+++ b/internal/server/web/static/css/mytoken.css
@@ -42,4 +42,12 @@ ul.list-group li.list-group-item {
     border-top: 1px solid rgba(0, 0, 0, .15);
     border-left: 1px solid rgba(0, 0, 0, .15);
     border-right: 1px solid rgba(0, 0, 0, .15);
+}
+
+.toggle, .toggle-on, .toggle-off {
+    border-radius: 0.3rem;
+}
+
+.toggle.round, .toggle-on.round, .toggle-off.round {
+    border-radius: 20rem;
 }
\ No newline at end of file
diff --git a/internal/server/web/static/css/restr-gui.css b/internal/server/web/static/css/restr-gui.css
index 20deac8d..a449bd81 100644
--- a/internal/server/web/static/css/restr-gui.css
+++ b/internal/server/web/static/css/restr-gui.css
@@ -1,12 +1,15 @@
-.table-item{
+.table-item {
     padding: 0.375rem 0.75rem;
 }
-.toggle{
-    width: 110px!important;
+
+.toggle.restr-editor, .toggle-on.restr-editor, .toggle-off.restr-editor {
+    width: 110px !important;
 }
-.btn-add-list-item{
+
+.btn-add-list-item {
     padding: 0;
 }
-.btn-delete-list-item{
+
+.btn-delete-list-item {
     padding: 0;
 }
diff --git a/internal/server/web/static/js/capabilities.js b/internal/server/web/static/js/capabilities.js
index 22a2352f..3f672aaa 100644
--- a/internal/server/web/static/js/capabilities.js
+++ b/internal/server/web/static/js/capabilities.js
@@ -9,6 +9,9 @@ const $capSummarySettings = $('#cap-summary-settings');
 const $capSummaryHowManyGreen = $('#cap-summary-count-green');
 const $capSummaryHowManyYellow = $('#cap-summary-count-yellow');
 const $capSummaryHowManyRed = $('#cap-summary-count-red');
+const $capRWModes = $('.rw-cap-mode');
+
+const rPrefix = "read@";
 
 $capabilityChecks.click(function () {
     checkThisCapability.call(this);
@@ -51,22 +54,28 @@ $(document).ready(function () {
         $('#subtokenCapabilities').hideB();
     }
     updateCapSummary();
+    $capRWModes.trigger('update-change');
 })
 
 function getCheckedCapabilities() {
-    return _getCheckedCapabilities($capabilityChecks);
+    return _getCheckedCapabilities($capabilityChecks, 'cp');
 }
 
 function getCheckedSubtokenCapabilities() {
     if (!$capabilityCreateMytoken.prop("checked")) {
         return [];
     }
-    return _getCheckedCapabilities($subtokenCapabilityChecks);
+    return _getCheckedCapabilities($subtokenCapabilityChecks, 'sub-cp');
 }
 
-function _getCheckedCapabilities($checks) {
+function _getCheckedCapabilities($checks, idPrefix) {
     let caps = $checks.filter(':checked').map(function (_, el) {
-        return $(el).val();
+        let v = $(el).val();
+        let $rw = $('#' + escapeSelector(idPrefix + '-' + rPrefix + v + '-mode'));
+        if ($rw.length && !$rw.prop('checked')) {
+            v = rPrefix + v;
+        }
+        return v;
     }).get();
     caps = caps.filter(filterCaps);
     return caps;
@@ -86,7 +95,6 @@ function filterCaps(c, i, caps) {
 }
 
 function isChildCapability(a, b) {
-    const rPrefix = "read@";
     let aReadOnly = a.startsWith(rPrefix);
     let bReadOnly = b.startsWith(rPrefix);
     if (aReadOnly) {
@@ -149,7 +157,7 @@ function updateCapSummary() {
             continue;
         }
         let name = $(c).val();
-        let $icon = $($(c).closest('li.list-group-item').find('i.fa-exclamation-circle')[0]);
+        let $icon = $($(c).closest('li.list-group-item').find('i.fa-exclamation-circle').not('.d-none')[0]);
         if ($icon.hasClass('text-success')) {
             counter['green'][name] = 1;
         }
@@ -199,4 +207,31 @@ function updateCapSummary() {
     } else {
         $capSummarySettings.attr('data-original-title', "This mytoken cannot be used to change settings.");
     }
-}
\ No newline at end of file
+}
+
+$capRWModes.on('update-change', function () {
+    let write = $(this).prop('checked');
+    if (write) {
+        $(this).closest('span').attr('data-original-title', `Allows full access. Click to only allow read access.`);
+        $(this).closest('div.flex-fill').find('.rw-cap-read').hideB();
+        $(this).closest('div.flex-fill').find('.rw-cap-write').showB();
+    } else {
+        $(this).closest('span').attr('data-original-title', `Allows only read access. Click to allow full access.`);
+        $(this).closest('div.flex-fill').find('.rw-cap-write').hideB();
+        $(this).closest('div.flex-fill').find('.rw-cap-read').showB();
+    }
+});
+
+$capRWModes.change(function () {
+    let write = $(this).prop('checked');
+    $(this).trigger('update-change');
+    let $modes = $(this).closest('li.list-group-item').find('.rw-cap-mode');
+    $modes.bootstrapToggle(write ? 'on' : 'off', true);
+    $modes.trigger('update-change');
+    if (!write) {
+        let $p = $(this).parents('li.list-group-item').children('div').find('.rw-cap-mode');
+        $p.bootstrapToggle('off', true);
+        $p.trigger('update-change');
+    }
+    updateCapSummary();
+});
\ No newline at end of file
diff --git a/internal/server/web/static/js/consent.js b/internal/server/web/static/js/consent.js
index 9dbcc9dc..dd2c10df 100644
--- a/internal/server/web/static/js/consent.js
+++ b/internal/server/web/static/js/consent.js
@@ -10,10 +10,20 @@ $(function (){
     }
     updateRotationIcon();
     checkedCapabilities.forEach(function (value) {
-        $('#cp-' + escapeSelector(value)).prop("checked", true)
+        let rCap = value.startsWith(rPrefix);
+        if (rCap) {
+            value = value.substring(rPrefix.length);
+        }
+        $('#cp-' + escapeSelector(value)).prop("checked", true);
+        $('#cp-' + escapeSelector(rPrefix + value) + '-mode').bootstrapToggle(rCap ? 'off' : 'on');
     })
     checkedSubtokenCapabilities.forEach(function (value) {
-        $('#sub-cp-' + escapeSelector(value)).prop("checked", true)
+        let rCap = value.startsWith(rPrefix);
+        if (rCap) {
+            value = value.substring(rPrefix.length);
+        }
+        $('#sub-cp-' + escapeSelector(value)).prop("checked", true);
+        $('#sub-cp-' + escapeSelector(rPrefix + value) + '-mode').bootstrapToggle(rCap ? 'off' : 'on');
     })
     chainFunctions(
         discovery,
diff --git a/internal/server/web/static/js/create-mt.js b/internal/server/web/static/js/create-mt.js
index dbee9ecd..049a2f2a 100644
--- a/internal/server/web/static/js/create-mt.js
+++ b/internal/server/web/static/js/create-mt.js
@@ -182,26 +182,22 @@ function polling(code, interval) {
                         return;
                     case "access_denied":
                         message = "You denied the authorization request.";
-                        window.clearInterval(intervalID);
                         break;
                     case "expired_token":
                         message = "Code expired. You might want to restart the flow.";
-                        window.clearInterval(intervalID);
                         break;
                     case "invalid_grant":
                     case "invalid_token":
                         message = "Code already used.";
-                        window.clearInterval(intervalID);
                         break;
                     case "undefined":
                         message = "No response from server";
-                        window.clearInterval(intervalID);
                         break;
                     default:
                         message = getErrorMessage(errRes);
-                        window.clearInterval(intervalID);
                         break;
                 }
+                window.clearInterval(intervalID);
                 mtShowError(message)
             }
         });
-- 
GitLab