Support plugin source on registration

Change-Id: I0ddc6ff0c0499db3a16114d0c0604fb9615d7127
diff --git a/Changes b/Changes
index d7cdf6c..f4cad8f 100755
--- a/Changes
+++ b/Changes
@@ -3,6 +3,7 @@
         - Added OAuth client authorization handling. (diewald)
         - Mark public clients as slightly more insecure. (diewald)
         - Reintroduce email handle support. (fixes #165; diewald)
+        - Support plugin declarations on registration. (diewald)
 
 0.44 2022-02-31
         - Fixed autosecrets migration. (diewald)
diff --git a/dev/scss/base/form.scss b/dev/scss/base/form.scss
index 8275f54..18ad3d4 100644
--- a/dev/scss/base/form.scss
+++ b/dev/scss/base/form.scss
@@ -123,6 +123,45 @@
     color:            $dark-green;
   }
 
+  span.file-upload {
+    @include choose-item;
+    box-shadow:   $choose-box-shadow;
+    border: $choose-border;
+    border-radius:     $standard-border-radius;
+    padding: $item-padding;
+    position: relative;
+    overflow: hidden;
+    right: 0;
+    display: inline-block;
+
+    &:hover {
+      @include choose-hover;
+      transition: none;
+    }
+   
+    > input[type=file] {
+      position: absolute;
+      top: 0;
+      left: 0;
+      margin: 0;
+      padding: 3em;
+      font-size: 20px;
+      cursor: pointer;
+      opacity: 0;
+      filter: alpha(opacity=0);
+      &.field-with-error {
+        background-color: $ids-pink-1;
+        opacity: .3;
+        filter: alpha(opacity=.3);
+      }
+    }
+
+    &::after {
+      @include icon-font;
+      content: $fa-upload;
+    }
+  }
+  
   a.form-button:hover {
     color: inherit !important;
   }
@@ -139,7 +178,7 @@
   border-color:     $darkest-orange !important;
 }
 
-button {
+button, input[type=submit] {
   cursor: pointer;
 
   + button {
@@ -162,8 +201,6 @@
       font-family: "FontAwesome";
     }
     
-    border: $border-size solid $nearly-white;
-
     &:hover,
     &:focus {
       @include choose-hover;
@@ -192,6 +229,17 @@
   }
 }
 
+button[type=submit] {
+  border: $border-size solid $nearly-white;
+}
+
+*[type=submit].form-submit {
+  box-shadow:   $choose-box-shadow;
+  border-radius:     $standard-border-radius;
+  border-width: 2px !important;
+  padding: $base-padding !important;
+}
+
 /**
  * Checkbox styling
  * http://stackoverflow.com/questions/4148499/how-to-style-checkbox-using-css
diff --git a/dev/scss/base/icons.scss b/dev/scss/base/icons.scss
index 4d4a37c..001b272 100644
--- a/dev/scss/base/icons.scss
+++ b/dev/scss/base/icons.scss
@@ -9,6 +9,7 @@
 $fa-minimize:     "\f0d8";
 $fa-close:        "\f00d";
 $fa-download:     "\f019";
+$fa-upload:       "\f093";
 $fa-info:         "\f05a";
 $fa-elipsis:      "\f141";
 $fa-previous:     "\f0d9";
diff --git a/kalamar.dict b/kalamar.dict
index 05db62d..6c51d26 100644
--- a/kalamar.dict
+++ b/kalamar.dict
@@ -36,6 +36,7 @@
     pwd => 'Passwort',
     email => 'Email',
     username => 'Benutzername',
+    upload => 'Hochladen',
     with => 'mit',
     glimpse => {
       desc => 'Zeige nur die ersten Treffer in beliebiger Reihenfolge'
@@ -124,6 +125,7 @@
     pwd => 'Password',
     email => 'Email',
     username => 'Username',
+    upload => 'Upload',
     with => 'with',
     notAvailInCorpus => 'Not available in the current corpus',
     pubOn => 'published on',
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 4bdb1f3..db28491 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -4,6 +4,7 @@
 use File::Spec::Functions qw/catdir/;
 use Mojo::ByteStream 'b';
 use Mojo::Util qw!deprecated b64_encode encode!;
+use Mojo::JSON 'decode_json';
 use Encode 'is_utf8';
 
 # This is a plugin to deal with the Kustvakt OAuth server.
@@ -78,6 +79,7 @@
           revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
           paramError => 'Einige Eingaben sind fehlerhaft',
           redirectUri => 'Weiterleitungs-Adresse',
+          pluginSrc => 'Beschreibung des Plugins (*.json-Datei)',
           homepage => 'Webseite',
           desc => 'Kurzbeschreibung',
           revoke => 'Widerrufen',
@@ -94,6 +96,7 @@
             -long => 'Möchten sie <span class="client-name"><%= $client_name %></span> wirklich löschen?',
             short => 'Löschen'
           },
+          oauthHint => 'Die folgende Registrierung (und alle Angaben) für API-Clients folgen der <a href="https://oauth.net/" class="external">OAuth-2.0-Spezifikation</a>.',
           loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
           oauthIssueToken => {
             -long => 'Stelle einen neuen Token für <span class="client-name"><%= $client_name %></span> aus',
@@ -109,7 +112,8 @@
             short => 'Zugriffsrechte erteilen'
           },
           createdAt => 'Erstellt am <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
-          expiresIn => 'Läuft in <%= stash("seconds") %> Sekunden ab.'
+          expiresIn => 'Läuft in <%= stash("seconds") %> Sekunden ab.',
+          fileSizeExceeded => 'Dateigröße überschritten'
         },
         -en => {
           loginPlease => 'Please log in!',
@@ -130,6 +134,7 @@
           revokeSuccess => 'Token was revoked successfully',
           paramError => 'Some fields are invalid',
           redirectUri => 'Redirect URI',
+          pluginSrc => 'Declaration of the plugin (*.json file)',
           homepage => 'Homepage',
           desc => 'Short description',
           revoke => 'Revoke',
@@ -146,6 +151,7 @@
             -long => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
             short => 'Unregister'
           },
+          oauthHint => 'The following registration of API clients follows the <a href="https://oauth.net/" class="external">OAuth 2.0 specification</a>.',
           loginHint => 'Maybe you need to log in first?',
           oauthIssueToken => {
             -long => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
@@ -161,7 +167,10 @@
             short => 'Grant access'
           },
           createdAt => 'Created at <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
-          expiresIn => 'Expires in <%= stash("seconds") %> seconds.'
+          expiresIn => 'Expires in <%= stash("seconds") %> seconds.',
+          fileSizeExceeded => 'File size exceeded',
+          confidentialRequired => 'Plugins need to be confidential',
+          jsonRequired => 'Plugin declarations need to be json files',
         }
       }
     }
@@ -234,7 +243,6 @@
     }
   );
 
-
   # Log in to the system
   my $r = $app->routes;
 
@@ -850,11 +858,14 @@
           };
 
           $v->csrf_protect;
-          $v->required('name', 'trim')->size(3, 255);
+          $v->required('name', 'trim', 'not_empty')->size(3, 255);
           $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
-          $v->required('desc', 'trim')->size(3, 255);
-          $v->optional('url', 'trim')->like(qr/^(http|$)/i);
-          $v->optional('redirect_uri', 'trim')->like(qr/^(http|$)/i);
+          $v->required('desc', 'trim', 'not_empty')->size(3, 255);
+          $v->optional('url', 'trim', 'not_empty')->like(qr/^(http|$)/i);
+          $v->optional('redirect_uri', 'trim', 'not_empty')->like(qr/^(http|$)/i);
+          $v->optional('src', 'not_empty');
+
+          $c->stash(template => 'auth/clients');
 
           # Render with error
           if ($v->has_error) {
@@ -864,8 +875,62 @@
             else {
               $c->notify(error => $c->loc('Auth_paramError'));
             };
-            # return $c->redirect_to('oauth-settings');
-            return $c->render(template => 'auth/clients');
+            return $c->render;
+          } elsif ($c->req->is_limit_exceeded) {
+            $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
+            return $c->render;
+          };
+
+          my $type = $v->param('type');
+          my $src = $v->param('src');
+          my $src_json;
+
+          my $json_obj = {
+            name         => $v->param('name'),
+            type         => $type,
+            description  => $v->param('desc'),
+            url          => $v->param('url'),
+            redirect_uri => $v->param('redirect_uri')
+          };
+
+          # Check plugin source
+          if ($src) {
+
+            # Plugins need to be confidential
+            if ($type ne 'CONFIDENTIAL') {
+              $c->notify(error => $c->loc('Auth_confidentialRequired'));
+              return $c->render;
+            }
+
+            # Source need to be a file upload
+            elsif (!ref $src || !$src->isa('Mojo::Upload')) {
+              $c->notify(error => $c->loc('Auth_jsonRequired'));
+              return $c->render;
+            };
+
+            # Uploads can't be too large
+            if ($src->size > 1_000_000) {
+              $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
+              return $c->render;
+            };
+
+            # Check upload is not empty
+            if ($src->size > 0 && $src->filename ne '') {
+
+              my $asset = $src->asset;
+
+              # Check for json
+              eval {
+                $src_json = decode_json($asset->slurp);
+              };
+
+              if ($@ || !ref $src_json || ref $src_json ne 'HASH') {
+                $c->notify(error => $c->loc('Auth_jsonRequired'));
+                return $c->render;
+              };
+
+              $json_obj->{source} = $src_json;
+            };
           };
 
           # Wait for async result
@@ -873,13 +938,7 @@
 
           # Register on server
           state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
-          $c->korap_request('POST', $url => {} => json => {
-            name        => $v->param('name'),
-            type        => $v->param('type'),
-            description => $v->param('desc'),
-            url         => $v->param('url'),
-            redirect_uri => $v->param('redirect_uri')
-          })->then(
+          $c->korap_request('POST', $url => {} => json => $json_obj)->then(
             sub {
               my $tx = shift;
               my $result = $tx->result;
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
index 4016919..e25d63f 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/clients.html.ep
@@ -20,11 +20,13 @@
 % };
 
 
-%= form_for 'oauth-register', class => 'form-table oauth-register', begin
+%= form_for 'oauth-register', class => 'form-table oauth-register', enctype => 'multipart/form-data', begin
   <fieldset>
     %= csrf_field
     <legend><%= loc('Auth_clientRegister') %></legend>
 
+    <p><%== loc 'Auth_oauthHint' %></p>
+    
     <div>
       %= label_for name => loc('Auth_clientName'), class => 'field-required', maxlength => 255
       %= text_field 'name'
@@ -45,15 +47,23 @@
     </div>
 
     <div>
-      %= label_for name => loc('Auth_homepage')
+      %= label_for url => loc('Auth_homepage')
       %= url_field 'url', placeholder => 'https://...'
     </div>
 
     <div>
-      %= label_for name => loc('Auth_redirectUri')
+      %= label_for redirect_uri => loc('Auth_redirectUri')
       %= url_field 'redirect_uri', placeholder => 'https://...'
     </div>
 
-    %= submit_button loc('Auth_clientRegister')
+    <div>
+      %= label_for src => loc('Auth_pluginSrc')
+      <span class="file-upload">
+        <span><%= loc('upload') %></span>
+        %= file_field 'src'
+      </span>
+    </div>
+    
+    %= submit_button loc('Auth_clientRegister'), class => 'form-submit'
   </fieldset>
 % end
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/grant_scope.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/grant_scope.html.ep
index ba58500..15b21f9 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/grant_scope.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/grant_scope.html.ep
@@ -20,6 +20,6 @@
    %= hidden_field 'scope' => stash('scope')
    % };
 
-   <input type="submit" value="<%= loc 'Auth_oauthGrantScope_short' %>" />
-   %= link_to loc('abort') => stash('redirect_uri_server') => {} => (class => 'form-button button-abort')
+   <input type="submit" class="form-submit" value="<%= loc 'Auth_oauthGrantScope_short' %>" />
+   %= link_to loc('abort') => stash('redirect_uri_server') => {} => (class => 'form-button button-abort form-submit')
 % end
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
index d79a7a7..cad593d 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
@@ -9,6 +9,6 @@
    %= hidden_field 'client-id' => stash('client_id')
    %= hidden_field 'name' => param('name')
    %#= hidden_field 'client-secret' 
-   <input type="submit" value="<%= loc 'Auth_oauthIssueToken_short' %>" />
-   %= link_to loc('abort') => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort')
+   <input type="submit" class="form-submit" value="<%= loc 'Auth_oauthIssueToken_short' %>" />
+   %= link_to loc('abort') => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort form-submit')
 % end
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
index d54b6f9..0f57bf4 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
@@ -8,6 +8,6 @@
    %= csrf_field
    %= hidden_field 'name' => param('name')
    %= hidden_field 'token' => param('token')
-   <input type="submit" value="<%= loc 'Auth_oauthRevokeToken_short' %>" />
-   %= link_to loc('abort') => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort')
+   <input type="submit" class="form-submit" value="<%= loc 'Auth_oauthRevokeToken_short' %>" />
+   %= link_to loc('abort') => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort form-submit')
 % end
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep
index a48f516..eea7db8 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/unregister.html.ep
@@ -7,6 +7,6 @@
 %= form_for 'oauth-unregister-post', class => 'form-table', begin
    %= csrf_field
    %= hidden_field 'client-name' => param('name')
-   <input type="submit" value="<%= loc 'Auth_oauthUnregister_short' %>" />
-   %= link_to loc('abort') => 'oauth-settings' => {} => (class => 'form-button button-abort')
+   <input type="submit" class="form-submit" value="<%= loc 'Auth_oauthUnregister_short' %>" />
+   %= link_to loc('abort') => 'oauth-settings' => {} => (class => 'form-button button-abort form-submit')
 % end
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index e12c394..478c164 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -478,6 +478,12 @@
 $t->get_ok('/settings/oauth')
   ->text_is('form.form-table legend', 'Register new client application')
   ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+  ->text_is('label[for=name]','Name of the client application')
+  ->text_is('label[for=type]','Type of the client application')
+  ->text_is('label[for=desc]','Short description')
+  ->text_is('label[for=url]','Homepage')
+  ->text_is('label[for=redirect_uri]','Redirect URI')
+  ->text_is('label[for=src]','Declaration of the plugin (*.json file)')
   ->element_exists('ul.client-list')
   ->element_exists_not('ul.client-list > li')
   ->header_is('Cache-Control','max-age=0, no-cache, no-store, must-revalidate')
@@ -499,7 +505,11 @@
   name => 'MyApp',
   type => 'CONFIDENTIAL',
   desc => 'This is my application',
-  csrf_token => $csrf
+  csrf_token => $csrf,
+  src => {
+    filename => '',
+    content => ''
+  }
 })
   ->status_is(200)
   ->element_exists('div.notify-success')
@@ -877,6 +887,52 @@
   ->header_is('location', 'http://example.com/?error_description=FAIL')
   ;
 
+my $json_post = {
+  name => 'Funny',
+  type => 'PUBLIC',
+  desc => 'This is my application',
+  csrf_token => $csrf,
+  src => 'hMMM'
+};
+
+$t->post_ok('/settings/oauth/register' => form => $json_post)
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Plugins need to be confidential')
+  ;
+
+$json_post->{type} = 'CONFIDENTIAL';
+
+$t->post_ok('/settings/oauth/register' => form => $json_post)
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Plugin declarations need to be json files')
+  ;
+
+$json_post->{src} = {
+  content => 'jjjjjj',
+  filename => 'fun.txt'
+};
+
+$t->post_ok('/settings/oauth/register' => form => $json_post)
+  ->status_is(200)
+  ->element_exists('div.notify-error')
+  ->text_is('div.notify-error', 'Plugin declarations need to be json files')
+  ;
+
+$json_post->{src} = {
+  content => '{"name":"example"}',
+  filename => 'fun.txt'
+};
+
+$t->post_ok('/settings/oauth/register' => form => $json_post)
+  ->status_is(200)
+  ->element_exists_not('div.notify-error')
+  ->element_exists('div.notify-success')
+  ->text_is('div.notify-success', 'Registration successful')
+  ;
+
+
 
 done_testing;
 __END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 7deb12a..8064208 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -544,6 +544,7 @@
   my $desc = $json->{description};
   my $type = $json->{type};
   my $url  = $json->{url};
+  my $src  = $json->{source};
   my $redirect_uri = $json->{redirect_uri};
 
   my $list = $c->app->defaults('oauth.client_list');
@@ -553,7 +554,8 @@
     "client_name" => $name,
     "client_description" => $desc,
     "client_url" => $url,
-    "client_redirect_uri" => $redirect_uri
+    "client_redirect_uri" => $redirect_uri,
+    "client_source" => $src
   };
 
   if ($redirect_uri && $redirect_uri =~ /FAIL$/) {