From f4e4febe2c073ad1afe9ca2fcda5d8fe55810d79 Mon Sep 17 00:00:00 2001
From: Eugen Ciur <eugen@papermerge.com>
Date: Fri, 11 Feb 2022 22:04:32 +0100
Subject: [PATCH] tokens management

---
 app/authenticators/auth-token.js             |   2 +-
 app/components/alert/success.hbs             |   4 +
 app/components/commander/index.hbs           |   2 +-
 app/components/tag/new.hbs                   | 104 +++++++++----------
 app/components/token/new.hbs                 |  47 +++++++++
 app/components/token/new.js                  |  85 +++++++++++++++
 app/components/token/table_row.hbs           |  14 +++
 app/components/token/table_row.js            |  13 +++
 app/helpers/truncatechars.js                 |  20 ++++
 app/models/token.js                          |  12 +++
 app/router.js                                |   7 +-
 app/routes/authenticated/tags.js             |   2 +-
 app/routes/authenticated/tokens.js           |  11 ++
 app/styles/app.scss                          |   6 +-
 app/templates/authenticated/tokens.hbs       |   7 ++
 app/templates/authenticated/tokens/add.hbs   |   1 -
 app/templates/authenticated/tokens/index.hbs |   1 -
 17 files changed, 275 insertions(+), 63 deletions(-)
 create mode 100644 app/components/alert/success.hbs
 create mode 100644 app/components/token/new.hbs
 create mode 100644 app/components/token/new.js
 create mode 100644 app/components/token/table_row.hbs
 create mode 100644 app/components/token/table_row.js
 create mode 100644 app/helpers/truncatechars.js
 create mode 100644 app/models/token.js
 create mode 100644 app/routes/authenticated/tokens.js
 create mode 100644 app/templates/authenticated/tokens.hbs
 delete mode 100644 app/templates/authenticated/tokens/add.hbs
 delete mode 100644 app/templates/authenticated/tokens/index.hbs

diff --git a/app/authenticators/auth-token.js b/app/authenticators/auth-token.js
index 4accc36..8a00242 100644
--- a/app/authenticators/auth-token.js
+++ b/app/authenticators/auth-token.js
@@ -27,7 +27,7 @@ export default class AuthToken extends Base {
   async authenticate(username, password) {
     let response, error;
 
-    response = await fetch(`${base_url()}/auth-token/`, {
+    response = await fetch(`${base_url()}/auth/login/`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
diff --git a/app/components/alert/success.hbs b/app/components/alert/success.hbs
new file mode 100644
index 0000000..ed92e0d
--- /dev/null
+++ b/app/components/alert/success.hbs
@@ -0,0 +1,4 @@
+<div class="alert alert-success alert-dismissible fade show" role="alert">
+  {{@message}}
+  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+</div>
\ No newline at end of file
diff --git a/app/components/commander/index.hbs b/app/components/commander/index.hbs
index 45ed1cb..fc80a2a 100644
--- a/app/components/commander/index.hbs
+++ b/app/components/commander/index.hbs
@@ -1,4 +1,4 @@
-<div class="panel commander col m-2 p-2"
+<div class="panel commander col m-2 p-2 user-select-none"
   {{droppable
     onDrop=this.onDrop
     onDragOver=this.onDragOver }}
diff --git a/app/components/tag/new.hbs b/app/components/tag/new.hbs
index e1fd42f..51aacff 100644
--- a/app/components/tag/new.hbs
+++ b/app/components/tag/new.hbs
@@ -12,65 +12,65 @@
       </div>
     </div>
 
-        <div class="row mb-2">
-            <div class="col-md-3">
-                <label for="name" class="form-label">Name</label>
-                <Input
-                    id="name"
-                    @type="text"
-                    @value={{this.new_name}}
-                    class="form-control"
-                />
-                <div class="d-flex flex-row my-1">
-                    <Input id="fg_color" @type="color" @value={{this.new_fg_color}} class="form-control form-control-color" />
-                    <Input id="bg_color" @type="color" @value={{this.new_bg_color}} class="form-control form-control-color" />
-                </div>
-            </div>
-            <div class="col-md-1">
-                <label class="form-check-label" for="is_pinned">Is Pinned?</label>
-                <div>
-                    <Input
-                        @type="checkbox"
-                        @checked={{this.new_pinned}}
-                        class="form-check-input" id="is_pinned" />
-                </div>
+    <div class="row mb-2">
+        <div class="col-md-3">
+            <label for="name" class="form-label">Name</label>
+            <Input
+                id="name"
+                @type="text"
+                @value={{this.new_name}}
+                class="form-control"
+            />
+            <div class="d-flex flex-row my-1">
+                <Input id="fg_color" @type="color" @value={{this.new_fg_color}} class="form-control form-control-color" />
+                <Input id="bg_color" @type="color" @value={{this.new_bg_color}} class="form-control form-control-color" />
             </div>
-            <div class="col-md-5">
-                <label for="description" class="form-label">Description</label>
+        </div>
+        <div class="col-md-1">
+            <label class="form-check-label" for="is_pinned">Is Pinned?</label>
+            <div>
                 <Input
-                    id="description"
-                    @type="text"
-                    @value={{this.new_description}}
-                    class="form-control"
-                />
+                    @type="checkbox"
+                    @checked={{this.new_pinned}}
+                    class="form-check-input" id="is_pinned" />
             </div>
-            <div class="col-md-3">
-                <label for="action" class="form-label">Action</label>
-                <div>
+        </div>
+        <div class="col-md-5">
+            <label for="description" class="form-label">Description</label>
+            <Input
+                id="description"
+                @type="text"
+                @value={{this.new_description}}
+                class="form-control"
+            />
+        </div>
+        <div class="col-md-3">
+            <label for="action" class="form-label">Action</label>
+            <div>
+                <button
+                    type="button"
+                    {{on "click" this.onCancel}}
+                    class="btn btn-secondary">
+                    Cancel
+                </button>
+                {{#if this.new_name }}
                     <button
                         type="button"
-                        {{on "click" this.onCancel}}
-                        class="btn btn-secondary">
-                        Cancel
+                        {{on "click" this.onCreate}}
+                        class="btn btn-success">
+                        Create Tag
                     </button>
-                    {{#if this.new_name }}
-                        <button
-                            type="button"
-                            {{on "click" this.onCreate}}
-                            class="btn btn-success">
-                            Create Tag
-                        </button>
-                    {{else}}
-                        <button
-                            type="button"
-                            {{on "click" this.onCreate}}
-                            disabled
-                            class="btn btn-success">
-                            Create Tag
-                        </button>
-                    {{/if}}
-                </div>
+                {{else}}
+                    <button
+                        type="button"
+                        {{on "click" this.onCreate}}
+                        disabled
+                        class="btn btn-success">
+                        Create Tag
+                    </button>
+                {{/if}}
             </div>
         </div>
+    </div>
     </form>
 {{/if}}
\ No newline at end of file
diff --git a/app/components/token/new.hbs b/app/components/token/new.hbs
new file mode 100644
index 0000000..7cda1e5
--- /dev/null
+++ b/app/components/token/new.hbs
@@ -0,0 +1,47 @@
+{{#if this.message_with_token}}
+  <Alert::Success @message={{this.message_with_token}} />
+{{/if}}
+
+<Button::New @onClick={{this.onToggleNew}} class="add-token" />
+
+{{#if this.form_visible}}
+  <form>
+    <div class="row mb-2">
+      <div class="col-md-3">
+        <label for="name" class="form-label">Expiry</label>
+        <div class="input-group mb-3">
+          <input type="number"
+            min="1"
+            max="365"
+            class="form-control"
+            value="{{this.new_expiry}}"
+            {{on 'change' this.onNumericChange}} >
+          <select class="form-select" {{on 'change' this.onScaleChange}} >
+            <option selected value="1">hour(s)</option>
+            <option value="24">day(s)</option>
+            <option value="744">month(s)</option>
+          </select>
+        </div>
+      </div>
+
+      <div class="col-md-3">
+        <label for="action" class="form-label">Action</label>
+        <div>
+          <button
+            type="button"
+            {{on "click" this.onCancel}}
+            class="btn btn-secondary">
+            Cancel
+          </button>
+
+          <button
+            type="button"
+            {{on "click" this.onCreate}}
+            class="btn btn-success">
+            Create Token
+          </button>
+        </div>
+      </div>
+    </div>
+  </form>
+{{/if}}
\ No newline at end of file
diff --git a/app/components/token/new.js b/app/components/token/new.js
new file mode 100644
index 0000000..1ec419b
--- /dev/null
+++ b/app/components/token/new.js
@@ -0,0 +1,85 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+
+
+export default class NewTokenComponent extends Component {
+  /*
+  Component to create new token.
+
+  It consists from a button labeled 'new' which
+  when clicked will toggle a 'new token' form.
+  */
+
+  new_expiry_numeric = 1;
+  new_expiry_scale = 1;
+  @service store;
+
+  // initially only 'new' button is visible
+  @tracked form_visible = false;
+  @tracked message_with_token;
+
+  @action
+  onToggleNew() {
+    this.form_visible = !this.form_visible;
+  }
+
+  get new_expiry() {
+    /*
+    Returns token expiry value, in hours.
+    */
+    let numeric, scale;
+
+    // the numeric value e.g. 3
+    numeric = parseInt(this.new_expiry_numeric);
+    // when user chooses hours, scale := 1
+    // when user chooses days, scale := 24
+    // when user chooses months, scale := 744
+    scale = parseInt(this.new_expiry_scale);
+
+    return numeric * scale;
+  }
+
+  @action
+  onCreate() {
+    let that = this;
+
+    this.store.createRecord('token', {
+        expiry_hours: this.new_expiry
+      }).save().then(model => {
+        let msg;
+
+        msg = "Please remember the token as it won't be displayed again: ";
+        msg += model.token;
+
+        that.message_with_token = msg;
+      });
+
+    this._empty_form();
+  }
+
+  @action
+  onCancel() {
+    this._empty_form();
+  }
+
+  @action
+  onNumericChange(event) {
+    this.new_expiry_numeric = parseInt(event.target.value);
+  }
+
+  @action
+  onScaleChange(event) {
+    this.new_expiry_scale = parseInt(event.target.value);
+  }
+
+  _empty_form() {
+    /*
+    Resets the form to initial state
+    */
+    this.new_expiry_scale = 1;
+    this.new_expiry_numeric = 1;
+    this.form_visible = false;
+  }
+}
diff --git a/app/components/token/table_row.hbs b/app/components/token/table_row.hbs
new file mode 100644
index 0000000..1506613
--- /dev/null
+++ b/app/components/token/table_row.hbs
@@ -0,0 +1,14 @@
+<tr>
+  <td>
+    {{truncatechars @token.digest}}
+  </td>
+  <td>
+    {{@token.expiry}}
+  </td>
+  <td>{{@token.created}}</td>
+  <td>
+    <button class="btn btn-link" type="button" {{on "click" (fn this.onRemove @token)}}>
+    Remove
+    </button>
+  </td>
+</tr>
\ No newline at end of file
diff --git a/app/components/token/table_row.js b/app/components/token/table_row.js
new file mode 100644
index 0000000..0fee1aa
--- /dev/null
+++ b/app/components/token/table_row.js
@@ -0,0 +1,13 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { service } from '@ember/service';
+
+
+export default class TokenTableRowComponent extends Component {
+  @service store;
+
+  @action
+  async onRemove(token) {
+    await token.destroyRecord();
+  }
+}
diff --git a/app/helpers/truncatechars.js b/app/helpers/truncatechars.js
new file mode 100644
index 0000000..be2f4ce
--- /dev/null
+++ b/app/helpers/truncatechars.js
@@ -0,0 +1,20 @@
+import { helper } from '@ember/component/helper';
+
+
+export function truncatechars([str]) {
+  /*
+    Truncates a string if it is longer than the specified number of characters.
+    Truncated strings will end with a translatable ellipsis character ('…').
+  */
+  if (!str) {
+    return '';
+  }
+
+  if (str.length <= 10) {
+    return str;
+  }
+
+  return `${str.substring(0, 10)}...`;
+}
+
+export default helper(truncatechars);
diff --git a/app/models/token.js b/app/models/token.js
new file mode 100644
index 0000000..4ddcb30
--- /dev/null
+++ b/app/models/token.js
@@ -0,0 +1,12 @@
+import Model, { attr } from '@ember-data/model';
+
+
+class TokenModel extends Model {
+  @attr expiry_hours;
+  @attr expiry;
+  @attr digest;
+  @attr token;
+  @attr created;
+}
+
+export default TokenModel;
diff --git a/app/router.js b/app/router.js
index 35b438d..c9e0b99 100644
--- a/app/router.js
+++ b/app/router.js
@@ -16,6 +16,8 @@ Router.map(function () {
 
     this.route('tags');
 
+    this.route('tokens');
+
     this.route('automates', function () {
       this.route('add');
       this.route('edit', { path: '/:automate_id/edit' });
@@ -44,10 +46,7 @@ Router.map(function () {
       this.route('section', { path: '/:section_name' });
     });
 
-    this.route('tokens', function() {
-      this.route('add');
-      this.route('index', { path: '/' });
-    });
+
   });
 
   this.route('login');
diff --git a/app/routes/authenticated/tags.js b/app/routes/authenticated/tags.js
index 67f9b06..0a6b8d5 100644
--- a/app/routes/authenticated/tags.js
+++ b/app/routes/authenticated/tags.js
@@ -1,4 +1,4 @@
-import { inject as service } from '@ember/service';
+import { service } from '@ember/service';
 import BaseRoute from 'papermerge/base/routing';
 
 
diff --git a/app/routes/authenticated/tokens.js b/app/routes/authenticated/tokens.js
new file mode 100644
index 0000000..924d608
--- /dev/null
+++ b/app/routes/authenticated/tokens.js
@@ -0,0 +1,11 @@
+import { service } from '@ember/service';
+import BaseRoute from 'papermerge/base/routing';
+
+
+export default class TokensRoute extends BaseRoute {
+  @service store;
+
+  async model() {
+    return this.store.findAll('token');
+  }
+}
diff --git a/app/styles/app.scss b/app/styles/app.scss
index 28733cb..ceac81d 100644
--- a/app/styles/app.scss
+++ b/app/styles/app.scss
@@ -14,8 +14,10 @@
 
 body {
     background-color: #e9ecef;
-    // make node title text unselectable
-    user-select: none;
+}
+
+.user-select-none {
+  user-select: none;
 }
 
 main {
diff --git a/app/templates/authenticated/tokens.hbs b/app/templates/authenticated/tokens.hbs
new file mode 100644
index 0000000..b22ac27
--- /dev/null
+++ b/app/templates/authenticated/tokens.hbs
@@ -0,0 +1,7 @@
+<Token::New />
+
+<Table @titles={{array 'Digest' 'Expiry' 'Created' 'Action' }}>
+  {{#each @model as |token|}}
+    <Token::TableRow @token={{token}} />
+  {{/each}}
+</Table>
diff --git a/app/templates/authenticated/tokens/add.hbs b/app/templates/authenticated/tokens/add.hbs
deleted file mode 100644
index a319583..0000000
--- a/app/templates/authenticated/tokens/add.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Add
\ No newline at end of file
diff --git a/app/templates/authenticated/tokens/index.hbs b/app/templates/authenticated/tokens/index.hbs
deleted file mode 100644
index 9f7cda8..0000000
--- a/app/templates/authenticated/tokens/index.hbs
+++ /dev/null
@@ -1 +0,0 @@
-Index
\ No newline at end of file
-- 
GitLab