dev/core#2141 - Basic UI for CRUD'ing clients
authorTim Otten <totten@civicrm.org>
Mon, 2 Nov 2020 06:17:06 +0000 (22:17 -0800)
committerTim Otten <totten@civicrm.org>
Tue, 3 Nov 2020 12:32:48 +0000 (04:32 -0800)
13 files changed:
ext/oauth-client/CRM/OAuth/Angular.php [new file with mode: 0644]
ext/oauth-client/ang/oauthClientAdmin.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientAdmin.aff.json [new file with mode: 0644]
ext/oauth-client/ang/oauthClientCreateHelp.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientCreator.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientEditor.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientList.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthClientTokens.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthProviderDetail.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthProviderList.aff.html [new file with mode: 0644]
ext/oauth-client/ang/oauthUtil.ang.php [new file with mode: 0644]
ext/oauth-client/ang/oauthUtil.js [new file with mode: 0644]
ext/oauth-client/info.xml

diff --git a/ext/oauth-client/CRM/OAuth/Angular.php b/ext/oauth-client/CRM/OAuth/Angular.php
new file mode 100644 (file)
index 0000000..4f0bc33
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+class CRM_OAuth_Angular {
+
+  public static function getSettings() {
+    $s = [];
+
+    $s['redirectUrl'] = \CRM_OAuth_BAO_OAuthClient::getRedirectUri();
+    $s['providers'] = civicrm_api4('OAuthProvider', 'get', [])->indexBy('name');
+
+    return $s;
+  }
+
+}
diff --git a/ext/oauth-client/ang/oauthClientAdmin.aff.html b/ext/oauth-client/ang/oauthClientAdmin.aff.html
new file mode 100644 (file)
index 0000000..234fa9b
--- /dev/null
@@ -0,0 +1,11 @@
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div id="bootstrap-theme">
+  <div ng-if="!routeParams.provider">
+    <div oauth-provider-list></div>
+  </div>
+
+  <div ng-if="routeParams.provider">
+    <div oauth-provider-detail="{provider: theProviders[routeParams.provider]}"></div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/ext/oauth-client/ang/oauthClientAdmin.aff.json b/ext/oauth-client/ang/oauthClientAdmin.aff.json
new file mode 100644 (file)
index 0000000..dbc4750
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "title": "OAuth2 Client Administration",
+  "server_route": "civicrm/admin/oauth",
+  "permission": "manage OAuth client"
+}
diff --git a/ext/oauth-client/ang/oauthClientCreateHelp.aff.html b/ext/oauth-client/ang/oauthClientCreateHelp.aff.html
new file mode 100644 (file)
index 0000000..4af0fa8
--- /dev/null
@@ -0,0 +1,32 @@
+<div oauth-util-import="CRM.oauthUtil.redirectUrl" to="redirectUrl"></div>
+<div class="help">
+  <p>{{ts('Please register with your web-service provider first. Be sure to:')}}</p>
+  <ul>
+    <li>
+      <p>
+        {{ts('Configure the "Redirect URL":')}}
+      </p>
+      <pre>{{redirectUrl}}</pre>
+      <div ng-if="redirectUrl.startsWith('http:/') && !redirectUrl.match('//(localhost|127\.0\.0\.1)')">
+        <p>
+          {{ts('WARNING: Most web-service providers require "https://" URLs. Alternatively, "http://" may be accepted for strictly local URLs ("localhost" or "127.0.0.1").')}}
+        </p>
+        <p>
+          {{ts('If you are doing development or testing on a local HTTP virtual-host, then consider a work-around like "bin/local-redir.sh".')}}
+        </p>
+      </div>
+    </li>
+
+    <li ng-if="options.provider.options.scopes.length > 0">
+      <p>
+        {{ts('Configure the scopes:')}}
+      </p>
+      <pre>{{options.provider.options.scopes.join("\n")}}</pre>
+    </li>
+
+    <li>
+      {{ts('Determine the client credentials ("Client ID" and "Client Secret").')}}
+    </li>
+  </ul>
+  <p>{{ts('Finally, copy the client credentials below:')}}</p>
+</div>
\ No newline at end of file
diff --git a/ext/oauth-client/ang/oauthClientCreator.aff.html b/ext/oauth-client/ang/oauthClientCreator.aff.html
new file mode 100644 (file)
index 0000000..15e7443
--- /dev/null
@@ -0,0 +1,14 @@
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div ng-form="create" class="form-horizontal">
+  <div class="form-group">
+    <div>
+      <label for="guid">{{ts('Client ID')}}:</label>
+      <input class="form-control" ng-model="options.client.guid" type="text" id="guid" />
+    </div>
+    <div>
+      <label for="secret">{{ts('Client Secret')}}:</label>
+      <input class="form-control" ng-model="options.client.secret" type="text" id="secret" />
+    </div>
+  </div>
+</div>
diff --git a/ext/oauth-client/ang/oauthClientEditor.aff.html b/ext/oauth-client/ang/oauthClientEditor.aff.html
new file mode 100644 (file)
index 0000000..7588460
--- /dev/null
@@ -0,0 +1,32 @@
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div ng-form="update" class="form-horizontal">
+  <div class="form-group">
+    <div>
+      <label for="provider{{options.client.id}}">{{ts('Provider')}}:</label>
+      <input class="form-control" ng-model="options.client.provider" disabled id="provider{{options.client.id}}">
+    </div>
+    <div>
+      <label for="id{{options.client.id}}">{{ts('Client ID (Private)')}}:</label>
+      <input class="form-control" ng-model="options.client.id" type="text" id="id{{options.client.id}}" disabled/>
+    </div>
+    <div>
+      <label for="guid{{options.client.id}}">{{ts('Client ID (Public)')}}:</label>
+      <input class="form-control" ng-model="options.client.guid" type="text" id="guid{{options.client.id}}"/>
+    </div>
+    <div>
+      <label for="secret{{options.client.id}}">{{ts('Client Secret')}}:</label>
+      <input class="form-control" ng-model="options.client.secret" type="text" id="secret{{options.client.id}}"/>
+    </div>
+    <div ng-if="options.client.created_date">
+      <label for="created_date{{options.client.id}}">{{ts('Created Date')}}:</label>
+      <input class="form-control" ng-model="options.client.created_date" disabled
+             id="created_date{{options.client.id}}">
+    </div>
+    <div ng-if="options.client.modified_date">
+      <label for="modified_date{{options.client.id}}">{{ts('Modified Date')}}:</label>
+      <input class="form-control" ng-model="options.client.modified_date" disabled
+             id="modified_date{{options.client.id}}">
+    </div>
+  </div>
+</div>
diff --git a/ext/oauth-client/ang/oauthClientList.aff.html b/ext/oauth-client/ang/oauthClientList.aff.html
new file mode 100644 (file)
index 0000000..923a439
--- /dev/null
@@ -0,0 +1,46 @@
+<div
+  af-api4="['OAuthClient', 'get', {select: ['id','provider','guid'], orderBy: {provider:'ASC'}}]"
+  af-api4-ctrl="listCtrl">
+
+  <div ng-if="apiData.result.length == 0">
+    {{ts('There are no clients!')}}
+  </div>
+
+  <table>
+    <thead>
+      <tr>
+        <th>{{ts('ID')}}</th>
+        <th>{{ts('Provider')}}</th>
+        <th>{{ts('GUID')}}</th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody>
+    <tr ng-repeat="availClient in listCtrl.result">
+      <td>
+        <a ng-href="#!/?id={{availClient.id}}">{{availClient.id}}</a>
+      </td>
+      <td>{{availClient.provider}}</td>
+      <td>{{availClient.guid}}</td>
+      <td>
+      <!--
+        <a af-api4-action="['Afform', 'revert', {where: [['name','=', availClient.name]]}]"
+           af-api4-start-msg="ts('Reverting...')"
+           af-api4-success-msg="ts('Reverted')"
+           af-api4-success="listCtrl.refresh()"
+           class="btn btn-xs btn-default"
+           ng-if="availClient.has_local && availClient.has_base"
+          >{{ts('Revert')}}</a>
+          -->
+        <a af-api4-action="['OAuthClient', 'delete', {where: [['id','=', availClient.id]]}]"
+           af-api4-start-msg="ts('Deleting...')"
+           af-api4-success-msg="ts('Deleted')"
+           af-api4-success="listCtrl.refresh()"
+           class="btn btn-xs btn-default"
+        >{{ts('Delete')}}</a>
+      </td>
+    </tr>
+    </tbody>
+  </table>
+
+</div>
diff --git a/ext/oauth-client/ang/oauthClientTokens.aff.html b/ext/oauth-client/ang/oauthClientTokens.aff.html
new file mode 100644 (file)
index 0000000..5d463e4
--- /dev/null
@@ -0,0 +1,33 @@
+<div af-api4-ctrl="tokens" af-api4="['OAuthSysToken', 'get', {'where': [['client_id', '=', options.clientId]]}]">
+</div>
+<div ng-if="tokens.result.length == 0">
+  {{ts('No tokens found')}}
+</div>
+
+<table class="table" ng-if="tokens.result.length > 0">
+  <tr>
+    <th>{{ts('ID')}}</th>
+    <th>{{ts('Tag')}}</th>
+    <th>{{ts('On Behalf Of')}}</th>
+    <th>{{ts('Scopes')}}</th>
+    <th>{{ts('Created Date')}}</th>
+    <th>{{ts('Actions')}}</th>
+  </tr>
+  <tr ng-repeat="token in tokens.result">
+    <td>{{token.id}}</td>
+    <td>{{token.tag}}</td>
+    <td>{{token.resource_owner_name}}</td>
+    <td>{{token.scopes.join(" ")}}</td>
+    <td>{{token.created_date}}</td>
+    <td>
+      <div class="btn-group">
+        <a class="btn btn-danger"
+           af-api4-action="['OAuthSysToken', 'delete', {where: [['id', '=', token.id]]}]"
+           af-api4-start-msg="ts('Deleting...')"
+           af-api4-success-msg="ts('Deleted')"
+           af-api4-success="tokens.refresh()"
+        >{{ts('Delete')}}</a>
+      </div>
+    </td>
+  </tr>
+</table>
diff --git a/ext/oauth-client/ang/oauthProviderDetail.aff.html b/ext/oauth-client/ang/oauthProviderDetail.aff.html
new file mode 100644 (file)
index 0000000..1b19df6
--- /dev/null
@@ -0,0 +1,55 @@
+<h1>{{options.provider.title}}</h1>
+
+<div af-api4-ctrl="theClients" af-api4="['OAuthClient', 'get', {where: [['provider','=',options.provider.name]]}]"></div>
+
+<div ng-if="!theClients.loading">
+  <div class="panel panel-info" ng-init="selected = {tab: theClients.result.length > 0 ? 'client_' + theClients.result[0].id : 'new'}">
+    <ul class="panel-heading nav nav-tabs">
+      <li role="presentation" ng-repeat="theClient in theClients.result" ng-class="{active: selected.tab === 'client_' + theClient.id}"><a ng-click="selected.tab = 'client_' + theClient.id">{{ts('Client #%1', {1: theClient.id})}}</a></li>
+      <li role="presentation" ng-class="{active: selected.tab === 'new'}"><a ng-click="selected.tab = 'new'">{{ts('Register Client')}}</a></li>
+      <li role="presentation" ng-class="{active: selected.tab === 'details'}"><a ng-click="selected.tab = 'details'">{{ts('Details')}}</a></li>
+    </ul>
+
+    <div class="panel-body" ng-if="selected.tab === 'details'">
+      <pre>{{options.provider|json}}</pre>
+    </div>
+
+    <div class="panel-body" ng-repeat="resultClient in theClients.result" ng-if="selected.tab === 'client_'+resultClient.id">
+      <div ng-form="editClientForm">
+        <h4>{{ts('Tokens')}}</h4>
+
+        <div oauth-client-tokens="{clientId: resultClient.id}"></div>
+
+        <div class="btn-group" oauth-util-grant-ctrl="granter">
+          <a class="btn btn-primary" ng-click="granter.authCode(resultClient.id)">{{ts('Add (Auth Code)')}}</a>
+        </div>
+
+        <h4>{{ts('Properties')}}</h4>
+
+        <div oauth-client-editor="{client: resultClient}"></div>
+        <div class="btn-group">
+          <a class="btn btn-primary"
+             af-api4-action="['OAuthClient', 'update', {where: [['id', '=', resultClient.id]], values:resultClient}]">{{ts('Save')}}</a>
+          <a class="btn btn-danger"
+             af-api4-action="['OAuthClient', 'delete', {where: [['id', '=', resultClient.id]]}]"
+             af-api4-success="selected.tab = 'details'; theClients.refresh()"
+          >{{ts('Delete')}}</a>
+        </div>
+
+      </div>
+    </div>
+
+    <div class="panel-body" ng-if="selected.tab === 'new'" ng-form="newClientForm" ng-init="theNew = {provider: options.provider.name}">
+      <div oauth-client-create-help="{provider: options.provider}"></div>
+      <div crm-ui-debug="theNew"></div>
+      <div oauth-client-creator="{client: theNew}"></div>
+      <div class="btn-group">
+        <a class="btn btn-primary"
+           af-api4-action="['OAuthClient', 'create', {values:theNew}]"
+           af-api4-success="theNew = {provider: options.provider.name}; theClients.refresh(); selected.tab = 'client_' + response[0].id"
+        >{{ts('Add')}}</a>
+      </div>
+    </div>
+  </div>
+
+</div>
diff --git a/ext/oauth-client/ang/oauthProviderList.aff.html b/ext/oauth-client/ang/oauthProviderList.aff.html
new file mode 100644 (file)
index 0000000..3b797a6
--- /dev/null
@@ -0,0 +1,30 @@
+<div oauth-util-import="CRM.oauthUtil.providers" to="theProviders"></div>
+
+<div class="help">
+  <p>
+    {{ts('CiviCRM may be configured as a client that interacts with remote web-services, such as Google Mail or Microsoft Exchange. Please choose the type of web-service you wish to connect to:')}}
+  </p>
+
+  <!--
+  To do so, you must first register with the service to obtain credentials (Client ID and Client Secret). Copy the assigned credentials below.
+  -->
+</div>
+
+<table>
+  <thead>
+  <tr>
+    <th>{{ts('Name')}}</th>
+    <th>{{ts('Title')}}</th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr ng-repeat="provider in theProviders">
+    <td>
+      <a ng-href="#!/?provider={{provider.name}}">{{provider.name}}</a>
+    </td>
+    <td>
+      <a ng-href="#!/?provider={{provider.name}}">{{provider.title}}</a>
+    </td>
+  </tr>
+  </tbody>
+</table>
diff --git a/ext/oauth-client/ang/oauthUtil.ang.php b/ext/oauth-client/ang/oauthUtil.ang.php
new file mode 100644 (file)
index 0000000..759f585
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+// This file declares an Angular module which can be autoloaded
+// in CiviCRM. See also:
+// \https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules/n
+return [
+  'js' => [
+    'ang/oauthUtil.js',
+    // 'ang/oauthUtil/*.js',
+    // 'ang/oauthUtil/*/*.js',
+  ],
+  // 'css' => ['ang/oauthUtil.css'],
+  // 'partials' => ['ang/oauthUtil'],
+  // 'requires' => ['crmUi', 'crmUtil'],
+  'settings' => [],
+  'settingsFactory' => ['CRM_OAuth_Angular', 'getSettings'],
+  'exports' => [
+    'oauth-util-import' => 'A',
+    'oauth-util-grant-ctrl' => 'A',
+  ],
+];
diff --git a/ext/oauth-client/ang/oauthUtil.js b/ext/oauth-client/ang/oauthUtil.js
new file mode 100644 (file)
index 0000000..f436510
--- /dev/null
@@ -0,0 +1,48 @@
+(function(angular, $, _) {
+  angular.module('oauthUtil', CRM.angRequires('oauthUtil'));
+  // Import data from the 'CRM.foo' settings.
+  // Ex: <div oauth-util-import="CRM.oauthUtil.providers" to="theProviders" />
+  angular.module('oauthUtil').directive('oauthUtilImport', function() {
+    return {
+      restrict: 'EA',
+      scope: {
+        to: '=',
+        oauthUtilImport: '@'
+      },
+      controller: function($scope, $parse) {
+        $scope.to = $parse($scope.oauthUtilImport)({CRM: CRM});
+      }
+    };
+  });
+  angular.module('oauthUtil').directive('oauthUtilGrantCtrl', function() {
+    return {
+      restrict: 'EA',
+      scope: {
+        oauthUtilGrantCtrl: '='
+      },
+      controllerAs: 'oauthUtilGrantCtrl',
+      controller: function($scope, $parse, crmBlocker, crmApi4, crmStatus) {
+        var block = crmBlocker();
+        var ctrl = this;
+        ctrl.authCode = function(clientId) {
+          var confirmOpt = {
+            message: ts('You are about to be redirected to an external site.'),
+            options: {no: ts('Cancel'), yes: ts('Continue')}
+          };
+          CRM.confirm(confirmOpt)
+            .on('crmConfirm:yes', function(){
+              var going = crmApi4('OAuthClient', 'authorizationCode', {
+                'landingUrl': window.location.href,
+                'where': [['id', '=', clientId]]
+              }).then(function(r){
+                window.location = r[0].url;
+              });
+              return block(crmStatus({start: ts('Redirecting...'), success: ts('Redirecting...')}, going));
+            });
+        };
+
+        $scope.oauthUtilGrantCtrl = this;
+      }
+    };
+  });
+})(angular, CRM.$, CRM._);
index cca744c2c4d175d6bf75b6fc437bfb2660eea492..f0b5fa530f5b827b4eb894f7f6f4ea1bbab727a7 100644 (file)
@@ -23,6 +23,9 @@
   <compatibility>
     <ver>5.0</ver>
   </compatibility>
+  <requires>
+    <ext version="~4.5">org.civicrm.afform</ext>
+  </requires>
   <comments>This is a new, undeveloped module</comments>
   <classloader>
     <psr4 prefix="Civi\" path="Civi"/>