@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.
- package/api.md +586 -360
- package/dist/terafy.es2019.js +11 -2
- package/dist/terafy.js +11 -2
- package/lib/projectFile.js +179 -0
- package/lib/terafy.client.js +72 -26
- package/lib/terafy.server.js +124 -53
- package/package.json +5 -4
- package/widgets/tera-file-select.vue +136 -0
- package/widgets/tera-library-select.vue +0 -8
package/lib/terafy.server.js
CHANGED
|
@@ -403,57 +403,8 @@ export default class TeraFyServer {
|
|
|
403
403
|
);
|
|
404
404
|
*/
|
|
405
405
|
|
|
406
|
-
|
|
407
|
-
|
|
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.
|
|
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
|
|
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>
|