@iebh/tera-fy 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -403,57 +403,8 @@ export default class TeraFyServer {
403
403
  );
404
404
  */
405
405
 
406
- var focusContent = document.createElement('div');
407
- focusContent.innerHTML = '<div>Authenticate with <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a></div>'
408
- + '<div class="mt-2"><a class="btn btn-light">Open Popup...</a></div>';
409
-
410
- // Attach click listner to internal button to re-popup the auth window (in case popups are blocked)
411
- focusContent.querySelector('a.btn').addEventListener('click', ()=>
412
- this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl))
413
- );
414
-
415
- // Create a deferred promise which will (eventually) resolve when the downstream window signals its ready
416
- let waitOnWindowAuth = promiseDefer();
417
-
418
- // Create a listener for the message from the downstream window to resolve the promise
419
- let listenMessages = ({data}) => {
420
- if (data.TERA && data.action == 'popupUserState' && data.user) { // Signal sent from landing page - we're logged in, yey!
421
- let $auth = app.service('$auth');
422
-
423
- // Accept user polyfill from opener
424
- $auth.state = 'user';
425
- $auth.ready = true;
426
- $auth.isLoggedIn = true;
427
- $auth.user = data.user;
428
-
429
- this.debug('Received user auth from popup window', {'$auth.user': $auth.user});
430
- waitOnWindowAuth.resolve();
431
- }
432
- };
433
- window.addEventListener('message', listenMessages);
434
-
435
- // Go fullscreen, try to open the auth window + prompt the user to retry (if popups are blocked) and wait for resolution
436
- await this.requestFocus(()=> {
437
- // Try opening the popup automatically - this will likely fail if the user has popup blocking enabled
438
- this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl));
439
-
440
- // Display a message to the user, offering the ability to re-open the popup if it was originally denied
441
- this.uiSplat(focusContent, {logo: true});
442
-
443
- this.debug('Begin auth-check deferred wait...');
444
- return waitOnWindowAuth.promise;
445
- });
446
-
447
- this.debug('Cleaning up popup auth');
448
-
449
- // Remove message subscription
450
- window.removeEventListener('message', listenMessages);
451
-
452
- // Disable overlay content
453
- this.uiSplat(false);
454
-
455
- // ... then refresh the project list as we're likely going to need it
456
- await app.service('$projects').refresh();
406
+ // Try to restore state via Popup workaround
407
+ await this.getUserViaEmbedWorkaround();
457
408
 
458
409
  // Go back to start of auth checking loop and repull the user data
459
410
  throw 'REDO';
@@ -479,6 +430,99 @@ export default class TeraFyServer {
479
430
  })
480
431
  }
481
432
 
433
+
434
+ /**
435
+ * In embed mode only - create a popup window and try to auth via that
436
+ *
437
+ * When in embed mode we can't store local state (Cookies without SameSite + LocalStorage etc.) so the only way to auth the user in the restricted envionment:
438
+ *
439
+ * 1. Try to read state from LocalStorage (if so, skip everything else)
440
+ * 2. Create a popup - which can escape the security container - and trigger a login
441
+ * 3. Listen locally for a message from the popup which it will transmit the authed user to its original window opener
442
+ * 3. Stash the state in LocalStorage to avoid this in future
443
+ *
444
+ * This workaround is only needed when developing with TERA in an embed window - i.e. local dev / stand alone websites
445
+ * Its annoying but I've tried everything else as a security method to get Non-Same-Origin sites to talk to each other
446
+ * - MC 2024-04-03
447
+ *
448
+ * @returns {Promise} A promise which resolves when the operation has completed
449
+ */
450
+ async getUserViaEmbedWorkaround() {
451
+ let lsState = window.localStorage.getItem('tera.embedUser');
452
+ if (lsState) {
453
+ try {
454
+ lsState = JSON.parse(lsState);
455
+
456
+ let $auth = app.service('$auth');
457
+ $auth.state = 'user';
458
+ $auth.ready = true;
459
+ $auth.isLoggedIn = true;
460
+ $auth.user = lsState;
461
+
462
+ this.debug('Restored local user state from LocalStorage', {'$auth.user': $auth.user});
463
+
464
+ await app.service('$projects').refresh();
465
+ return;
466
+ } catch (e) {
467
+ throw new Error(`Failed to decode local dev state - ${e.toString()}`);
468
+ }
469
+ }
470
+
471
+ let focusContent = document.createElement('div');
472
+ focusContent.innerHTML = '<div>Authenticate with <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a></div>'
473
+ + '<div class="mt-2"><a class="btn btn-light">Open Popup...</a></div>';
474
+
475
+ // Attach click listner to internal button to re-popup the auth window (in case popups are blocked)
476
+ focusContent.querySelector('a.btn').addEventListener('click', ()=>
477
+ this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl))
478
+ );
479
+
480
+ // Create a deferred promise which will (eventually) resolve when the downstream window signals its ready
481
+ let waitOnWindowAuth = promiseDefer();
482
+
483
+ // Create a listener for the message from the downstream window to resolve the promise
484
+ let listenMessages = ({data}) => {
485
+ this.debug('Recieved message from popup window', {data});
486
+ if (data.TERA && data.action == 'popupUserState' && data.user) { // Signal sent from landing page - we're logged in, yey!
487
+ let $auth = app.service('$auth');
488
+
489
+ // Accept user polyfill from opener
490
+ $auth.state = 'user';
491
+ $auth.ready = true;
492
+ $auth.isLoggedIn = true;
493
+ $auth.user = data.user;
494
+
495
+ window.localStorage.setItem('tera.embedUser', JSON.stringify(data.user));
496
+
497
+ this.debug('Received user auth from popup window', {'$auth.user': $auth.user});
498
+ waitOnWindowAuth.resolve();
499
+ }
500
+ };
501
+ window.addEventListener('message', listenMessages);
502
+
503
+ // Go fullscreen, try to open the auth window + prompt the user to retry (if popups are blocked) and wait for resolution
504
+ await this.requestFocus(()=> {
505
+ // Try opening the popup automatically - this will likely fail if the user has popup blocking enabled
506
+ this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl));
507
+
508
+ // Display a message to the user, offering the ability to re-open the popup if it was originally denied
509
+ this.uiSplat(focusContent, {logo: true});
510
+
511
+ this.debug('Begin auth-check deferred wait...');
512
+ return waitOnWindowAuth.promise;
513
+ });
514
+
515
+ this.debug('Cleaning up popup auth');
516
+
517
+ // Remove message subscription
518
+ window.removeEventListener('message', listenMessages);
519
+
520
+ // Disable overlay content
521
+ this.uiSplat(false);
522
+
523
+ // ... then refresh the project list as we're likely going to need it
524
+ await app.service('$projects').refresh();
525
+ }
482
526
  // }}}
483
527
 
484
528
  // Projects - getProject(), getProjects(), requireProject(), selectProject() {{{
@@ -811,7 +855,7 @@ export default class TeraFyServer {
811
855
  }
812
856
  // }}}
813
857
 
814
- // Project files - selectProjectFile(), getProjectFiles() {{{
858
+ // Project files - selectProjectFile(), getProjectFiles(), getProjectFile(), setProjectFile() {{{
815
859
 
816
860
  /**
817
861
  * Data structure for a project file
@@ -921,6 +965,33 @@ export default class TeraFyServer {
921
965
  meta: settings.meta,
922
966
  }))
923
967
  }
968
+
969
+
970
+ /**
971
+ * Fetch a project file
972
+ * @param {String} path File path to read
973
+ * @returns {Promise<Blob>} The eventual fetched file as a blob
974
+ */
975
+ getProjectFile(path) {
976
+ return app.service('$supabase').fileGet(path, {
977
+ toast: false,
978
+ });
979
+ }
980
+
981
+
982
+ /**
983
+ * Replace a project files contents
984
+ *
985
+ * @param {String} path File path to write
986
+ * @param {File|Blob|FormData|Object|Array} contents The new file contents
987
+ * @returns {Promise} A promise which will resolve when the write operation has completed
988
+ */
989
+ setProjectFile(path, contents) {
990
+ return app.service('$supabase').fileGet(path, {
991
+ overwrite: true,
992
+ toast: false,
993
+ });
994
+ }
924
995
  // }}}
925
996
 
926
997
  // Project Libraries - selectProjectLibrary(), parseProjectLibrary(), setProjectLibrary() {{{
@@ -980,7 +1051,7 @@ export default class TeraFyServer {
980
1051
  let settings = {
981
1052
  format: 'pojo',
982
1053
  autoRequire: true,
983
- filter: file => true,
1054
+ filter: file => true, // eslint-disable-line
984
1055
  find: files => files.at(0),
985
1056
  ...options,
986
1057
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
6
  "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --serve --servedir=.",
@@ -8,9 +8,9 @@
8
8
  "build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify",
9
9
  "build:client:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle lib/terafy.client.js --outfile=dist/terafy.es2019.js --minify",
10
10
  "build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify",
11
- "build:docs:api": "documentation build lib/terafy.client.js --format html --config documentation.yml --output docs/",
12
- "build:docs:markdown": "documentation build lib/terafy.client.js --format md --markdown-toc --output api.md",
13
- "watch": "nodemon --watch lib/terafy.client.js --exec npm run build",
11
+ "build:docs:api": "documentation build lib/terafy.client.js lib/projectFile.js --format html --config documentation.yml --output docs/",
12
+ "build:docs:markdown": "documentation build lib/terafy.client.js lib/projectFile.js --format md --markdown-toc --output api.md",
13
+ "watch": "nodemon --watch lib --exec npm run build",
14
14
  "lint": "eslint ."
15
15
  },
16
16
  "type": "module",
@@ -65,6 +65,7 @@
65
65
  "node": ">=18"
66
66
  },
67
67
  "dependencies": {
68
+ "filesize": "^10.1.1",
68
69
  "just-diff": "^6.0.2",
69
70
  "lodash-es": "^4.17.21",
70
71
  "mitt": "^3.0.1",
@@ -0,0 +1,136 @@
1
+ <script>
2
+ /**
3
+ * Simple Vue widget to pick a project file via TERA-fy
4
+ * Most of the options are copied from the $teraFy.selectProjectFile() API call that this component wraps
5
+ * @see https://github.com/IEBH/TERA-fy
6
+ *
7
+ * @param {String} [title="Select a citation library"] The title of the file selection display
8
+ * @param {String|Array<String>} [hint] Hints to identify the library to select in array order of preference. Generally corresponds to the previous stage
9
+ * @param {Boolean} [allowUpload=true] Allow uploading new files
10
+ * @param {Boolean} [allowRefresh=true] Allow the user to manually refresh the file list
11
+ * @param {Boolean} [allowDownloadZip=true] Allow the user to download a Zip of all files
12
+ * @param {Boolean} [allowCancel=true] Allow cancelling the operation. Will throw `'CANCEL'` as the promise rejection if acationed
13
+ * @param {Boolean} [autoRequire=true] Run `requireProject()` automatically before continuing
14
+ * @param {FileFilters} [filters] Optional file filters, defaults to citation library selection only
15
+ * @param {String} [placeholder="Select a file..."] Placeholder text to show when no file is selected
16
+ *
17
+ * @fires change Fired as `(file:ProjectFile)` when the contents changes
18
+ *
19
+ * @slot selected Slot to show when a file is selected. Contains the bindings `({selected})`
20
+ * @slot deselected Slot to show when no file is selected. Contains the bindings `({})`
21
+ */
22
+ export default {
23
+ data() { return {
24
+ /**
25
+ * The currently selected library, if any
26
+ * @type {null|ProjectFile}
27
+ */
28
+ selected: null,
29
+ }},
30
+ props: {
31
+ // Props passed to $teraFy.selectProjectFile()
32
+ title: {type: String, default: 'Select a citation library'},
33
+ hint: {type: [String, Array]},
34
+ allowUpload: {type: Boolean, default: true},
35
+ allowRefresh: {type: Boolean, default: true},
36
+ allowDownloadZip: {type: Boolean, default: true},
37
+ allowCancel: {type: Boolean, default: true},
38
+ autoRequire: {type: Boolean, default: true},
39
+ filters: {type: Object, default: ()=> ({
40
+ library: true,
41
+ })},
42
+
43
+ // Props specific to this component
44
+ placeholder: {type: String, default: 'Select a file...'},
45
+ },
46
+ methods: {
47
+ /**
48
+ * Trigger the file selection functionality within TERA-fy
49
+ * This sets the `selected` data property to the newly selected file + fires @change
50
+ *
51
+ * @fires change Fired as `(file:ProjectFile)` when the contents changes
52
+ * @returns {ProjectFile} file The selected file
53
+ */
54
+ async choose() {
55
+ this.selected = await this.$tera.selectProjectFile({
56
+ title: this.title,
57
+ hint: this.hint,
58
+ allowUpload: this.allowUpload,
59
+ allowRefresh: this.allowRefresh,
60
+ allowDownloadZip: this.allowDownloadZip,
61
+ allowCancel: this.allowCancel,
62
+ autoRequire: this.autoRequire,
63
+ filters: this.filters,
64
+ });
65
+
66
+ this.$emit('change', this.selected);
67
+ return this.selected;
68
+ },
69
+ },
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ class="tera-library-select"
76
+ :class="selected && 'active'"
77
+ @click="choose"
78
+ >
79
+ <slot v-if="selected" name="selected" :selected="selected">
80
+ <a class="tera-library-select-selected">
81
+ <div class="file-name">
82
+ {{selected.parsedName.filename}}
83
+ </div>
84
+ <div class="file-meta">
85
+ (last edit {{selected.modifiedFormatted}} / {{selected.sizeFormatted}})
86
+ </div>
87
+ </a>
88
+ </slot>
89
+ <slot v-else name="deselected">
90
+ <a class="tera-library-select-deselected">
91
+ <div class="file-placeholder">
92
+ {{placeholder}}
93
+ </div>
94
+ </a>
95
+ </slot>
96
+ </div>
97
+ </template>
98
+
99
+ <style>
100
+ /* NOTE: This is basic, minimal CSS to maintain compatibility downstream - so no SCSS, just plain */
101
+
102
+ .tera-library-select {
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+
107
+ padding: 6px 10px;
108
+ border-radius: 5px;
109
+ width: 100%;
110
+ border: 1px solid #ced4da;
111
+ cursor: pointer;
112
+ min-height: 55px;
113
+ }
114
+
115
+ .tera-library-select a {
116
+ text-decoration: none;
117
+ color: inherit;
118
+ }
119
+
120
+ .tera-library-select a:hover {
121
+ text-decoration: none;
122
+ }
123
+
124
+ .tera-library-select.active {
125
+ border-color: #28a745;
126
+ }
127
+
128
+ .tera-library-select:hover .file-name,
129
+ .tera-library-select:hover .file-placeholder {
130
+ color: #0056b3;
131
+ }
132
+
133
+ .tera-library-select .file-meta {
134
+ font-size: xx-small;
135
+ }
136
+ </style>
@@ -1,8 +0,0 @@
1
- <script>
2
- export default {
3
- }
4
- </script>
5
-
6
- <template>
7
- <div>PLACEHOLDER: Library select</div>
8
- </template>