domma-cms 0.101.7 → 0.101.9
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/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/collections-arrange.js +1 -1
- package/admin/js/templates/collections.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-authentication.html +1 -1
- package/admin/js/templates/docs/api-builder.html +1 -1
- package/admin/js/templates/docs/api-collections.html +7 -4
- package/admin/js/templates/docs/api-external.html +1 -1
- package/admin/js/templates/docs/api-forms.html +1 -1
- package/admin/js/templates/docs/api-layouts.html +1 -1
- package/admin/js/templates/docs/api-media.html +1 -1
- package/admin/js/templates/docs/api-navigation.html +1 -1
- package/admin/js/templates/docs/api-pages.html +1 -1
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-scaffold.html +1 -1
- package/admin/js/templates/docs/api-settings.html +1 -1
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/api-views.html +1 -1
- package/admin/js/templates/docs/usage-cta-shortcode.html +2 -2
- package/package.json +2 -2
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +1 -1
- package/plugins/blog/blocks/blog-post-classic.html +1 -1
- package/plugins/blog/blocks/blog-post-feature.html +1 -1
- package/plugins/blog/blocks/blog-post-minimal.html +1 -1
- package/plugins/blog/blocks/blog-post-sidebar.html +1 -1
- package/plugins/blog/blocks/blog-post-split.html +1 -1
- package/plugins/blog/plugin.public.js +2 -2
- package/plugins/free-tier.lock.json +10 -10
- package/plugins/shopping-cart/plugin.public.js +1 -1
- package/server/routes/api/collections.js +7 -2
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +4 -4
- package/server/server.js +10 -0
- package/server/services/connectionSecrets.js +160 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools CSS v0.
|
|
2
|
+
* Domma Tools CSS v0.51.0
|
|
3
3
|
* Dynamic Object Manipulation & Modeling API
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-09-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-09-29T07:41:00.052Z
|
|
6
|
+
* Commit: f264aeb
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/*!
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools v0.
|
|
2
|
+
* Domma Tools v0.51.0
|
|
3
3
|
* Developer tools: Theme Roller & Page Roller
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-09-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-09-29T07:40:46.433Z
|
|
6
|
+
* Commit: f264aeb
|
|
7
7
|
*
|
|
8
8
|
* Requires: domma.min.js
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SYSTEM_SLUGS=["roles","user-profiles"],FILTERS=["all","own","builtin","plugin"];const u="core";export function slugify(n){return String(n??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export function kindOf(n){return n?.builtIn===!0||n?.preset===!0||SYSTEM_SLUGS.includes(n?.slug)?"builtin":n?.plugin?"plugin":"own"}export const projectOf=n=>n?.meta?.project||u;export function arrangeCollections(n,{filter:
|
|
1
|
+
export const SYSTEM_SLUGS=["roles","user-profiles"],FILTERS=["all","own","builtin","plugin"];const u="core";export function slugify(n){return String(n??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export function kindOf(n){return n?.builtIn===!0||n?.preset===!0||SYSTEM_SLUGS.includes(n?.slug)?"builtin":n?.plugin?"plugin":"own"}export const projectOf=n=>n?.meta?.project||u;export function arrangeCollections(n,{filter:t="all",query:e="",project:o=null}={}){const i=String(e).trim().toLowerCase();return[...n||[]].filter(r=>!o||projectOf(r)===o).filter(r=>t==="all"||kindOf(r)===t).filter(r=>!i||[r.title,r.slug,r.description,r.plugin].some(l=>String(l||"").toLowerCase().includes(i))).sort((r,l)=>String(r.title||r.slug).localeCompare(String(l.title||l.slug)))}export function kindCounts(n,{project:t=null}={}){const e=(n||[]).filter(i=>!t||projectOf(i)===t),o={all:e.length,own:0,builtin:0,plugin:0};for(const i of e)o[kindOf(i)]++;return o}const a=(n,t,e=`${t}s`)=>`${n} ${n===1?t:e}`;export function collectionRow(n){const t=kindOf(n),e=Array.isArray(n.fields)?n.fields.length:0,o=Number(n.entryCount)||0,i=n.storage?.adapter==="mongodb",r=n.meta?.project||"";return{slug:n.slug,title:n.title||n.slug,meta:[n.slug,a(e,"field"),r?`project ${r}`:""].filter(Boolean).join(" \xB7 "),entries:o,entriesText:o.toLocaleString("en-GB"),mongo:i,mongoTitle:i?`MongoDB - connection "${n.storage.connection||"default"}"`:"",builtin:t==="builtin",bundled:t!=="builtin"&&n.bundled===!0,plugin:n.plugin||"",pluginTitle:n.plugin?`Managed by the ${n.plugin} plugin`:"",canDelete:t!=="builtin",hasForm:!SYSTEM_SLUGS.includes(n.slug)}}export function checkNewCollection(n,t=[]){const e=String(n??"").trim(),o=slugify(e);return e?o?t.includes(o)?{slug:o,error:`"${o}" is already a collection.`}:{slug:o,error:""}:{slug:o,error:"The title needs at least one letter or number."}:{slug:"",error:""}}let s=0;export function connectionRows(n){return Object.entries(n||{}).filter(([t])=>t!=="_comment").map(([t,e])=>({id:`c${++s}`,name:t,uri:String(e?.uri||""),database:String(e?.database||""),options:e?.options&&typeof e.options=="object"?e.options:{}}))}export function newConnectionRow(n){const t=new Set((n||[]).map(o=>o.name));let e=(n||[]).length+1;for(;t.has(`connection${e}`);)e++;return{id:`c${++s}`,name:e===1&&!t.has("default")?"default":`connection${e}`,uri:"",database:""}}export function connectionsFromRows(n){const t=[],e={};for(const o of n||[]){const i=String(o.name??"").trim();if(!i){t.push("Every connection needs a name.");continue}if(e[i]){t.push(`Two connections are called "${i}".`);continue}const r=String(o.uri??"").trim(),l=String(o.database??"").trim();r||t.push(`"${i}" needs a URI.`),l||t.push(`"${i}" needs a database.`),e[i]={type:"mongodb",uri:r,database:l,options:o.options&&typeof o.options=="object"?o.options:{}}}return{errors:t,config:e}}export function parseConnectionsJson(n){let t;try{t=JSON.parse(String(n??"").trim()||"{}")}catch(e){return{error:`Not valid JSON: ${e.message}`,config:null}}return!t||typeof t!="object"||Array.isArray(t)?{error:"The JSON must be an object of named connections.",config:null}:{error:"",config:t}}export function maskUri(n){return String(n??"").replace(/(\/\/[^:/@]+:)([^@]+)(@)/,"$1\u2022\u2022\u2022$3")}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div class="clx-card">
|
|
41
41
|
<div class="clx-bar">
|
|
42
42
|
<strong>MongoDB connections</strong>
|
|
43
|
-
<span data-help-title="Database connections" data-help="Collections are stored as files unless their storage is set to MongoDB, which needs a connection here - usually one called default. Each needs a URI (mongodb://…) and a database name. Kept in config/connections.json; with none, every collection stays on files. Ctrl+S saves."></span>
|
|
43
|
+
<span data-help-title="Database connections" data-help="Collections are stored as files unless their storage is set to MongoDB, which needs a connection here - usually one called default. Each needs a URI (mongodb://…) and a database name. Passwords are never shown: a saved one appears as dots - leave them to keep it, or type a new one. Change the host, user or options and you must type it again. Kept in config/connections.json; with none, every collection stays on files. Ctrl+S saves."></span>
|
|
44
44
|
<span class="clx-spacer"></span>
|
|
45
45
|
<label class="clx-toggle"><input type="checkbox" data-model="raw.value"> Raw JSON</label>
|
|
46
46
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="addConnection" data-bind-hidden="raw.value">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Actions API</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Authentication</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · API Builder</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Collections</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -150,11 +150,14 @@
|
|
|
150
150
|
<h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/collections/connections</span></h3>
|
|
151
151
|
<p class="auth-note">Requires Bearer token + an admin role (level 0 or 1).</p>
|
|
152
152
|
<p>The MongoDB connections in <code>config/connections.json</code>. <code>PUT</code> saves them; each needs
|
|
153
|
-
<code>type</code>, <code>uri</code> and <code>database</code
|
|
153
|
+
<code>type</code>, <code>uri</code> and <code>database</code>. Passwords are never sent: the one in a URI, and any
|
|
154
|
+
secret-looking option, comes back as <code>••••••••</code>. Send the mask back unchanged to keep the stored value; it is restored only while scheme, user, hosts, query
|
|
155
|
+
and options are unchanged.</p>
|
|
154
156
|
<pre class="code-block"><code>// Response 200
|
|
155
|
-
{ "default": { "type": "mongodb", "uri": "mongodb://localhost:
|
|
157
|
+
{ "default": { "type": "mongodb", "uri": "mongodb://site:••••••••@localhost:27018/my_cms", "database": "my_cms" } }
|
|
156
158
|
// PUT - Error 400
|
|
157
|
-
{ "error": "Connection \"default\" requires type, uri, and database" }
|
|
159
|
+
{ "error": "Connection \"default\" requires type, uri, and database" }
|
|
160
|
+
{ "error": "Connection \"default\": type the password again - it was hidden, and the connection now points somewhere else or it was never saved" }</code></pre>
|
|
158
161
|
|
|
159
162
|
<h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
|
|
160
163
|
Admin Entry CRUD</h3>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · External API</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Forms API</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Layouts</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Media</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Navigation</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Pages</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Plugins</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Scaffold API</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Settings</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Users</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="code"></span> API
|
|
2
|
+
<h1><span data-icon="code"></span> API · Views API</h1>
|
|
3
3
|
<a href="#/api-reference" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> API overview</a>
|
|
4
4
|
</div>
|
|
5
5
|
<style>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<td><code>style</code></td>
|
|
52
52
|
<td>No</td>
|
|
53
53
|
<td><code>"primary"</code></td>
|
|
54
|
-
<td>primary
|
|
54
|
+
<td>primary · secondary · ghost · danger</td>
|
|
55
55
|
</tr>
|
|
56
56
|
<tr>
|
|
57
57
|
<td><code>icon</code></td>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<td><code>size</code></td>
|
|
64
64
|
<td>No</td>
|
|
65
65
|
<td>-</td>
|
|
66
|
-
<td>sm
|
|
66
|
+
<td>sm · md · lg</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td><code>confirm</code></td>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "0.101.
|
|
3
|
+
"version": "0.101.9",
|
|
4
4
|
"description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/server.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@fastify/rate-limit": "^10.3.0",
|
|
85
85
|
"@fastify/static": "9.1.1",
|
|
86
86
|
"bcryptjs": "^3.0.3",
|
|
87
|
-
"domma-js": "0.
|
|
87
|
+
"domma-js": "0.51.0",
|
|
88
88
|
"dotenv": "^17.2.3",
|
|
89
89
|
"fastify": "5.8.5",
|
|
90
90
|
"gray-matter": "^4.0.3",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<a class="br-media {{imageClass}}" href="{{url}}" tabindex="-1" aria-hidden="true">{{{image}}}</a>
|
|
6
6
|
<div class="br-body">
|
|
7
|
-
<p class="br-meta"><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time>
|
|
7
|
+
<p class="br-meta"><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
8
8
|
<h2 class="br-title"><a href="{{url}}">{{title}}</a></h2>
|
|
9
9
|
<p class="br-excerpt">{{excerpt}}</p>
|
|
10
10
|
<div class="br-foot"><span class="br-cats">{{{categories}}}</span><a class="br-author" href="{{authorUrl}}">{{authorName}}</a></div>
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
<div class="bc-kicker">{{{categories}}}</div>
|
|
13
13
|
<h2 class="bc-title"><a href="{{url}}">{{title}}</a></h2>
|
|
14
14
|
<p class="bc-excerpt">{{excerpt}}</p>
|
|
15
|
-
<p class="bc-meta"><a href="{{authorUrl}}">{{authorName}}</a>
|
|
15
|
+
<p class="bc-meta"><a href="{{authorUrl}}">{{authorName}}</a> · <time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
16
16
|
</div>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<div class="pc-kicker">{{{categories}}}</div>
|
|
15
15
|
<h1 class="pc-title">{{title}}</h1>
|
|
16
16
|
<p class="pc-standfirst">{{excerpt}}</p>
|
|
17
|
-
<p class="pc-meta">By <a href="{{authorUrl}}">{{authorName}}</a>
|
|
17
|
+
<p class="pc-meta">By <a href="{{authorUrl}}">{{authorName}}</a> · <time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
18
18
|
</header>
|
|
19
19
|
<figure class="pc-figure {{imageClass}}">{{{image}}}</figure>
|
|
20
20
|
<div class="pc-body blog-content">{{{content}}}</div>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<div class="pf-kicker">{{{categories}}}</div>
|
|
15
15
|
<h1 class="pf-title">{{title}}</h1>
|
|
16
16
|
<p class="pf-standfirst">{{excerpt}}</p>
|
|
17
|
-
<p class="pf-meta"><a href="{{authorUrl}}">{{authorName}}</a>
|
|
17
|
+
<p class="pf-meta"><a href="{{authorUrl}}">{{authorName}}</a> · <time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
18
18
|
</div>
|
|
19
19
|
</header>
|
|
20
20
|
<div class="pf-wrap">
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
-->
|
|
10
10
|
<article class="pm">
|
|
11
11
|
<header class="pm-head">
|
|
12
|
-
<p class="pm-meta"><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time>
|
|
12
|
+
<p class="pm-meta"><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
13
13
|
<h1 class="pm-title">{{title}}</h1>
|
|
14
14
|
<p class="pm-by">by <a href="{{authorUrl}}">{{authorName}}</a></p>
|
|
15
15
|
</header>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<p class="ps-label">Written by</p>
|
|
26
26
|
<p class="ps-author"><a href="{{authorUrl}}">{{authorName}}</a></p>
|
|
27
27
|
<p class="ps-label">Published</p>
|
|
28
|
-
<p><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time>
|
|
28
|
+
<p><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
29
29
|
<div class="ps-tags">{{{tags}}}</div>
|
|
30
30
|
{{{share}}}
|
|
31
31
|
</div>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<div class="pt-kicker">{{{categories}}}</div>
|
|
17
17
|
<h1 class="pt-title">{{title}}</h1>
|
|
18
18
|
<p class="pt-standfirst">{{excerpt}}</p>
|
|
19
|
-
<p class="pt-meta"><a href="{{authorUrl}}">{{authorName}}</a><br><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time>
|
|
19
|
+
<p class="pt-meta"><a href="{{authorUrl}}">{{authorName}}</a><br><time datetime="{{publishedAt}}">{{publishedAt:DD/MM/YYYY}}</time> · {{readingTime}}</p>
|
|
20
20
|
</div>
|
|
21
21
|
</header>
|
|
22
22
|
<div class="pt-wrap">
|
|
@@ -133,7 +133,7 @@ ${items || ''}
|
|
|
133
133
|
},
|
|
134
134
|
{title, urlPath: request.url.split('?')[0], baseUrl: originOf(request)}
|
|
135
135
|
);
|
|
136
|
-
return reply.type('text/html').send(html);
|
|
136
|
+
return reply.type('text/html; charset=utf-8').send(html);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** One handler per page; `matchBlogPath()` (lib/layouts.js) picks which. */
|
|
@@ -231,7 +231,7 @@ ${items}
|
|
|
231
231
|
item: {type: 'blog:post', id: post.id}
|
|
232
232
|
}
|
|
233
233
|
);
|
|
234
|
-
return reply.type('text/html').send(html);
|
|
234
|
+
return reply.type('text/html; charset=utf-8').send(html);
|
|
235
235
|
}
|
|
236
236
|
};
|
|
237
237
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@45a0ace",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
6
|
"version": "1.9.3",
|
|
@@ -22,21 +22,21 @@
|
|
|
22
22
|
"blocks/blog-card-compact.css": "eb8e8ae9a6461c362bc021ea0396c6b4481047b6b36a50eeb59e47375e49be75",
|
|
23
23
|
"blocks/blog-card-compact.html": "7e1e31923808e6108d57d34309d3442b5d93b4aa4396f0e471e870249934ab8f",
|
|
24
24
|
"blocks/blog-card-row.css": "f5f0249c51a40cc71ff8d054bfa887353badc79730b0f323c76ac32f7d893b44",
|
|
25
|
-
"blocks/blog-card-row.html": "
|
|
25
|
+
"blocks/blog-card-row.html": "c08148ab5b5addc86f1148841c21ffb11f9a1b94f28fbdc08bc9650167920374",
|
|
26
26
|
"blocks/blog-card.css": "d480a80a7378bd55659e9549bb30d8c106bdae1a9cd450a8c0114bfaefa94cd2",
|
|
27
|
-
"blocks/blog-card.html": "
|
|
27
|
+
"blocks/blog-card.html": "61e222fe36c6fe9167f2e0b5750a9ddb724c90c0c4719d4a671197baf525f52f",
|
|
28
28
|
"blocks/blog-post-classic.css": "9acac617847b5a10dabd5efccb601572e2751f792c4038475325f11e24142584",
|
|
29
|
-
"blocks/blog-post-classic.html": "
|
|
29
|
+
"blocks/blog-post-classic.html": "3fb19c554108e34c6602d2b01715067a2e181544ff206193dd9116482d0d852b",
|
|
30
30
|
"blocks/blog-post-essay.css": "d1842443927ea7c93772a06ade69fc834099e6820d99767e0aab8e80981a3784",
|
|
31
31
|
"blocks/blog-post-essay.html": "85bb2eafd07192f97fdf7a9690e80aab266edd43277d64fa0e91ebbc37c2d243",
|
|
32
32
|
"blocks/blog-post-feature.css": "7d8a242b627f3826df21d5470e643b386967122ea3585e69f96cce9ffc2452e9",
|
|
33
|
-
"blocks/blog-post-feature.html": "
|
|
33
|
+
"blocks/blog-post-feature.html": "7b38c8f50c7c31db2506abc672856cad6ac522597ae9ce551320083e910d053e",
|
|
34
34
|
"blocks/blog-post-minimal.css": "2323b84a7d0a99e1c85c30faa18bb2ee16763ff08cfe87f181ffadf8cff84dd0",
|
|
35
|
-
"blocks/blog-post-minimal.html": "
|
|
35
|
+
"blocks/blog-post-minimal.html": "71e3c461b5f8ac7f72055810bae611045aac84148dcf8f227a9d526bfecf0b49",
|
|
36
36
|
"blocks/blog-post-sidebar.css": "3c66109fa48ca448605eb5acd0c998f43649fb3747813b4a811992ec89886007",
|
|
37
|
-
"blocks/blog-post-sidebar.html": "
|
|
37
|
+
"blocks/blog-post-sidebar.html": "1e18f334950617cb38d5fb22c7d9ba29d76b744a28e761997c9f4e03de10187b",
|
|
38
38
|
"blocks/blog-post-split.css": "61ae29850d6c324ffbfc2110a917d5980ad17b0635870656ccd885d20ac9ec86",
|
|
39
|
-
"blocks/blog-post-split.html": "
|
|
39
|
+
"blocks/blog-post-split.html": "65e291e203bf7b9c7e07e705a56605d5a4c316781bdcbb0cb8c537d0b8281478",
|
|
40
40
|
"collections/categories/schema.json": "0728e522ed8e3c623fd45e137cd03e0960aa5158e70613ec8628c8b3928e0929",
|
|
41
41
|
"collections/comments/schema.json": "29cbd289cbfcfc4158c9357a218db4ce06eddc3370bfbf9afb7d7f52065a849e",
|
|
42
42
|
"collections/posts/schema.json": "19a5a770d3eefea73f4117454bd1312a7a7e3d727cfcdfde141139fbc81cabd7",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
|
|
52
52
|
"plugin.js": "0aa46a01b5298065497f39729b88e34a9f56ac684b0a14f8d94ab6007f0221d0",
|
|
53
53
|
"plugin.json": "f74ba12c5dfef7d0b9accd267cc0c4e333ea61a936cc8afa565db82da5b02389",
|
|
54
|
-
"plugin.public.js": "
|
|
54
|
+
"plugin.public.js": "67e109da613237d9ec52e7cc2f3442bb61ca5a51cb184448b9f0d993e50f7ae3",
|
|
55
55
|
"public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
|
|
56
56
|
"public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
|
|
57
57
|
"public/samples/aurora.svg": "feb29e2b46465913fc29de9eda221b00b1541f1fa3ea7120f0c1d1cd11a9a4f6",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"lib/summary.js": "a510386e28eb19d9617f488551d1057819ae9f76fc85c744f2e5dbce492607fe",
|
|
158
158
|
"plugin.js": "594c2174e36566f90ac1e9ff11cb76656ad111dc392dc8e9556d731b7dd0a41a",
|
|
159
159
|
"plugin.json": "1d76fea187dea958b82e752e5d6e7a729217b08b061490c8590e2c6e6537496e",
|
|
160
|
-
"plugin.public.js": "
|
|
160
|
+
"plugin.public.js": "ad901378eb28758d18b9f257e4a76d5a453f89fb984ed02a10f62f1a13cd2a62",
|
|
161
161
|
"public/cart.js": "fcc706773a36e4bfd02ea235bcdad629137e996218e0a0e23892e33c689f2a6e",
|
|
162
162
|
"public/lib/money.js": "d22b2b6f80dde6f7887b69e24188c05ea93c02217aba639b5331aa3bd616f4fc",
|
|
163
163
|
"public/lib/orders.js": "09478e20d238adc8e387b3d9efcb13c441898fab18387ce6a1a8a92304ac4c5b",
|
|
@@ -55,7 +55,7 @@ export default async function shopPublic(fastify) {
|
|
|
55
55
|
title, description, ogImage: image, urlPath, noindex,
|
|
56
56
|
baseUrl: `${request.protocol}://${request.host || request.headers.host || request.hostname}`
|
|
57
57
|
});
|
|
58
|
-
return reply.type('text/html').header('Cache-Control', 'no-store').send(html);
|
|
58
|
+
return reply.type('text/html; charset=utf-8').header('Cache-Control', 'no-store').send(html);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const pages = {
|
|
@@ -63,6 +63,7 @@ import {ensureFormForCollection} from '../../services/forms.js';
|
|
|
63
63
|
import {hooks} from '../../services/hooks.js';
|
|
64
64
|
import {scopeAllows, validateToken} from '../../services/apiTokens.js';
|
|
65
65
|
import {isProjectEnabled, resolveArtefactProject} from '../../services/projects.js';
|
|
66
|
+
import {maskConnections, restoreConnections} from '../../services/connectionSecrets.js';
|
|
66
67
|
|
|
67
68
|
const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION_SLUGS]);
|
|
68
69
|
|
|
@@ -323,7 +324,7 @@ export async function collectionsRoutes(fastify) {
|
|
|
323
324
|
fastify.get('/collections/connections', {preHandler: [authenticate, requireAdmin]}, async () => {
|
|
324
325
|
try {
|
|
325
326
|
const {_comment, ...rest} = getConfig('connections');
|
|
326
|
-
return rest;
|
|
327
|
+
return maskConnections(rest);
|
|
327
328
|
} catch {
|
|
328
329
|
return {};
|
|
329
330
|
}
|
|
@@ -339,7 +340,11 @@ export async function collectionsRoutes(fastify) {
|
|
|
339
340
|
return reply.status(400).send({error: `Connection "${name}" requires type, uri, and database`});
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
|
-
|
|
343
|
+
let stored = {};
|
|
344
|
+
try { stored = getConfig('connections'); } catch { /* nothing saved yet */ }
|
|
345
|
+
const {config, error} = restoreConnections(data, stored);
|
|
346
|
+
if (error) return reply.status(400).send({error});
|
|
347
|
+
saveConfig('connections', stored._comment !== undefined ? {_comment: stored._comment, ...config} : config);
|
|
343
348
|
return {success: true};
|
|
344
349
|
});
|
|
345
350
|
|
|
@@ -26,16 +26,16 @@ export async function docsPublicRoutes(fastify) {
|
|
|
26
26
|
// Hard kill-switch: docs disabled → 404 the entire surface.
|
|
27
27
|
if (!(await isProjectEnabled(DOCS_PROJECT_SLUG))) {
|
|
28
28
|
reply.status(404);
|
|
29
|
-
return reply.type('text/html').send(await render404(request.url.split('?')[0]));
|
|
29
|
+
return reply.type('text/html; charset=utf-8').send(await render404(request.url.split('?')[0]));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const docPath = String(request.params['*'] || '');
|
|
33
33
|
const html = await renderDocPage(docPath, {user: null, baseUrl: getBaseUrl(request)});
|
|
34
34
|
if (html == null) {
|
|
35
35
|
reply.status(404);
|
|
36
|
-
return reply.type('text/html').send(await render404(`/domma-docs${docPath ? '/' + docPath : ''}`));
|
|
36
|
+
return reply.type('text/html; charset=utf-8').send(await render404(`/domma-docs${docPath ? '/' + docPath : ''}`));
|
|
37
37
|
}
|
|
38
|
-
return reply.type('text/html').send(html);
|
|
38
|
+
return reply.type('text/html; charset=utf-8').send(html);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
fastify.get('/domma-docs', handle); // exact - params['*'] === '' (home landing)
|
package/server/routes/public.js
CHANGED
|
@@ -87,7 +87,7 @@ function sendHtml(request, reply, html) {
|
|
|
87
87
|
// Deliberately no reply.code() here: the 404 handlers have already called
|
|
88
88
|
// reply.status(404), and forcing 200 would turn every not-found into a
|
|
89
89
|
// soft 404 that search engines index as a real page.
|
|
90
|
-
return reply.type('text/html').send(html);
|
|
90
|
+
return reply.type('text/html; charset=utf-8').send(html);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export async function publicRoutes(fastify) {
|
|
@@ -170,7 +170,7 @@ export async function publicRoutes(fastify) {
|
|
|
170
170
|
shared: {expiresAt: link.expiresAt, label: link.label}
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
|
-
return reply.type('text/html').send(html);
|
|
173
|
+
return reply.type('text/html; charset=utf-8').send(html);
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
// Public pages catch-all
|
|
@@ -246,7 +246,7 @@ export async function publicRoutes(fastify) {
|
|
|
246
246
|
|
|
247
247
|
if (!isPublic && !checkVisibility(userObj, vis)) {
|
|
248
248
|
reply.status(403);
|
|
249
|
-
return reply.type('text/html').send(accessDeniedHtml(urlPath));
|
|
249
|
+
return reply.type('text/html; charset=utf-8').send(accessDeniedHtml(urlPath));
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
const baseUrl = getBaseUrl(request);
|
|
@@ -270,7 +270,7 @@ export async function publicRoutes(fastify) {
|
|
|
270
270
|
if (isDraft) {
|
|
271
271
|
reply.header('cache-control', 'no-store, private');
|
|
272
272
|
reply.header('x-robots-tag', 'noindex, nofollow');
|
|
273
|
-
return reply.type('text/html').send(await render());
|
|
273
|
+
return reply.type('text/html; charset=utf-8').send(await render());
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
// A row-scoped [view] shows each signed-in user their OWN rows, so the
|
package/server/server.js
CHANGED
|
@@ -164,6 +164,16 @@ await app.register(compress, {
|
|
|
164
164
|
encodings: ['br', 'gzip', 'deflate']
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
// Ensure any text/html responses always declare charset=utf-8 to prevent browsers,
|
|
168
|
+
// proxies, and scrapers from falling back to ISO-8859-1 or Windows-1252.
|
|
169
|
+
app.addHook('onSend', async (request, reply, payload) => {
|
|
170
|
+
const contentType = reply.getHeader('content-type');
|
|
171
|
+
if (typeof contentType === 'string' && contentType.startsWith('text/html') && !contentType.toLowerCase().includes('charset')) {
|
|
172
|
+
reply.header('content-type', `${contentType}; charset=utf-8`);
|
|
173
|
+
}
|
|
174
|
+
return payload;
|
|
175
|
+
});
|
|
176
|
+
|
|
167
177
|
// ---------------------------------------------------------------------------
|
|
168
178
|
// Static Assets
|
|
169
179
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection secrets never reach the browser.
|
|
3
|
+
*
|
|
4
|
+
* GET /collections/connections sends each URI with its password, and any secret query
|
|
5
|
+
* parameter (tlsCertificateKeyFilePassword, proxyPassword, authMechanismProperties), replaced
|
|
6
|
+
* by CONNECTION_MASK, and any option whose key looks secret (auth.password) as the mask too.
|
|
7
|
+
* PUT sends the mask back for anything left untouched and restoreConnections() puts the stored
|
|
8
|
+
* value back - but only from a stored connection with the same endpoint (scheme, user, host
|
|
9
|
+
* list, query and driver options, secrets aside). Change where or how a connection
|
|
10
|
+
* authenticates and its secrets must be typed again, or a masked URI aimed at another host or
|
|
11
|
+
* proxy would carry the stored password there. The database path may change freely. A mask
|
|
12
|
+
* that cannot be matched is refused, never saved as the password. A URI whose credentials
|
|
13
|
+
* cannot be told apart from its query (a raw ? in the password) is masked whole.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const CONNECTION_MASK = '••••••••';
|
|
17
|
+
|
|
18
|
+
const SECRET_KEY = /pass(word)?|secret|token|api[-_]?key|private|credential/i;
|
|
19
|
+
const SECRET_PARAM = /pass(word)?|secret|token|authMechanismProperties/i;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* scheme://[user[:password]@]hosts[/path][?query]. The query is split off first and the LAST
|
|
23
|
+
* @ before it ends the credentials, so an @ in the query or an unescaped one in the password
|
|
24
|
+
* cannot move the split.
|
|
25
|
+
*/
|
|
26
|
+
function parseUri(uri) {
|
|
27
|
+
if (typeof uri !== 'string') return null;
|
|
28
|
+
const m = uri.trim().match(/^([a-z][a-z0-9+.-]*:\/\/)(.*)$/is);
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
const rest = m[2];
|
|
31
|
+
const qi = rest.indexOf('?');
|
|
32
|
+
const main = qi < 0 ? rest : rest.slice(0, qi);
|
|
33
|
+
const at = main.lastIndexOf('@');
|
|
34
|
+
// An @ only after the first ? is either in the query or behind a raw ? in the password:
|
|
35
|
+
// unknowable, so the whole URI is treated as one secret.
|
|
36
|
+
if (rest === CONNECTION_MASK || (at < 0 && qi >= 0 && rest.indexOf('@', qi) >= 0)) {
|
|
37
|
+
return {opaque: true, scheme: m[1].toLowerCase(), uri: uri.trim()};
|
|
38
|
+
}
|
|
39
|
+
const userinfo = at < 0 ? null : main.slice(0, at);
|
|
40
|
+
const hp = at < 0 ? main : main.slice(at + 1);
|
|
41
|
+
const si = hp.indexOf('/');
|
|
42
|
+
const ci = userinfo === null ? -1 : userinfo.indexOf(':');
|
|
43
|
+
return {
|
|
44
|
+
scheme: m[1].toLowerCase(),
|
|
45
|
+
user: userinfo === null ? null : (ci < 0 ? userinfo : userinfo.slice(0, ci)),
|
|
46
|
+
pass: ci < 0 ? null : userinfo.slice(ci + 1),
|
|
47
|
+
hosts: si < 0 ? hp : hp.slice(0, si),
|
|
48
|
+
path: si < 0 ? '' : hp.slice(si),
|
|
49
|
+
params: qi < 0 ? null : rest.slice(qi + 1).split('&').map(p => {
|
|
50
|
+
const e = p.indexOf('=');
|
|
51
|
+
return e < 0 ? [p, null] : [p.slice(0, e), p.slice(e + 1)];
|
|
52
|
+
})
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatUri(p) {
|
|
57
|
+
if (p.opaque) return p.scheme + CONNECTION_MASK;
|
|
58
|
+
const cred = p.user === null ? '' : p.user + (p.pass === null ? '' : ':' + p.pass) + '@';
|
|
59
|
+
const query = p.params === null ? '' : '?' + p.params.map(([k, v]) => (v === null ? k : `${k}=${v}`)).join('&');
|
|
60
|
+
return p.scheme + cred + p.hosts + p.path + query;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const maskParams = (params) => params && params.map(([k, v]) => [k, v && SECRET_PARAM.test(k) ? CONNECTION_MASK : v]);
|
|
64
|
+
|
|
65
|
+
// Key order must not matter when comparing options.
|
|
66
|
+
const canonical = (v) => Array.isArray(v) ? v.map(canonical)
|
|
67
|
+
: v && typeof v === 'object' ? Object.fromEntries(Object.keys(v).sort().map(k => [k, canonical(v[k])])) : v;
|
|
68
|
+
|
|
69
|
+
/** Where and how a connection authenticates, secrets aside - whether stored secrets may follow it. */
|
|
70
|
+
function endpoint(p, options) {
|
|
71
|
+
const opts = canonical(maskOptions(options ?? {}));
|
|
72
|
+
if (p.opaque) return JSON.stringify(['opaque', p.scheme, opts]);
|
|
73
|
+
return JSON.stringify([p.scheme, p.user, p.hosts.toLowerCase(), maskParams(p.params), opts]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A URI with its password and secret query parameters masked. */
|
|
77
|
+
export function maskConnectionUri(uri) {
|
|
78
|
+
const p = parseUri(uri);
|
|
79
|
+
if (!p) return uri;
|
|
80
|
+
if (p.opaque) return formatUri(p);
|
|
81
|
+
return formatUri({...p, pass: p.pass ? CONNECTION_MASK : p.pass, params: maskParams(p.params)});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function maskOptions(value, key = '') {
|
|
85
|
+
if (Array.isArray(value)) return value.map(v => maskOptions(v, key));
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskOptions(v, k)]));
|
|
88
|
+
}
|
|
89
|
+
return SECRET_KEY.test(key) && typeof value === 'string' && value !== '' ? CONNECTION_MASK : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A mask with nothing stored behind it stays a mask, so the caller refuses the save.
|
|
93
|
+
function restoreOptions(incoming, stored) {
|
|
94
|
+
if (incoming === CONNECTION_MASK) return typeof stored === 'string' ? stored : incoming;
|
|
95
|
+
if (Array.isArray(incoming)) {
|
|
96
|
+
const before = Array.isArray(stored) ? stored : [];
|
|
97
|
+
return incoming.map((v, i) => restoreOptions(v, before[i]));
|
|
98
|
+
}
|
|
99
|
+
if (incoming && typeof incoming === 'object') {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
102
|
+
out[k] = restoreOptions(v, stored && typeof stored === 'object' ? stored[k] : undefined);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
return incoming;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function restoreUri(incoming, stored) {
|
|
110
|
+
const pi = parseUri(incoming);
|
|
111
|
+
const ps = parseUri(stored);
|
|
112
|
+
if (pi.opaque) return ps.uri;
|
|
113
|
+
const pass = pi.pass === CONNECTION_MASK ? ps.pass : pi.pass;
|
|
114
|
+
const params = pi.params && pi.params.map(([k, v], i) => [k, v === CONNECTION_MASK ? ps.params[i][1] : v]);
|
|
115
|
+
return formatUri({...pi, pass, params});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The connections as the browser may see them. */
|
|
119
|
+
export function maskConnections(connections) {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [name, conn] of Object.entries(connections || {})) {
|
|
122
|
+
if (!conn || typeof conn !== 'object') { out[name] = conn; continue; }
|
|
123
|
+
const masked = {...conn, uri: maskConnectionUri(conn.uri)};
|
|
124
|
+
if (conn.options !== undefined) masked.options = maskOptions(conn.options);
|
|
125
|
+
out[name] = masked;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The connections to save: masks put back from what is stored.
|
|
132
|
+
* @returns {{config?: object, error?: string}}
|
|
133
|
+
*/
|
|
134
|
+
export function restoreConnections(incoming, stored) {
|
|
135
|
+
const before = stored && typeof stored === 'object' ? stored : {};
|
|
136
|
+
const config = {};
|
|
137
|
+
for (const [name, conn] of Object.entries(incoming)) {
|
|
138
|
+
if (!conn || typeof conn !== 'object' || !JSON.stringify(conn).includes(CONNECTION_MASK)) {
|
|
139
|
+
config[name] = conn;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const pi = parseUri(conn.uri);
|
|
143
|
+
// The same name first, then any other stored connection at the same endpoint (a rename).
|
|
144
|
+
const candidates = [before[name], ...Object.entries(before).filter(([n]) => n !== name).map(([, c]) => c)];
|
|
145
|
+
const source = pi && candidates.find(c => {
|
|
146
|
+
const ps = c && typeof c === 'object' ? parseUri(c.uri) : null;
|
|
147
|
+
return ps && endpoint(ps, c.options) === endpoint(pi, conn.options);
|
|
148
|
+
});
|
|
149
|
+
const out = {...conn};
|
|
150
|
+
if (source) {
|
|
151
|
+
out.uri = restoreUri(conn.uri, source.uri);
|
|
152
|
+
if (conn.options !== undefined) out.options = restoreOptions(conn.options, source.options);
|
|
153
|
+
}
|
|
154
|
+
if (JSON.stringify(out).includes(CONNECTION_MASK)) {
|
|
155
|
+
return {error: `Connection "${name}": type the password again - it was hidden, and the connection now points somewhere else or it was never saved`};
|
|
156
|
+
}
|
|
157
|
+
config[name] = out;
|
|
158
|
+
}
|
|
159
|
+
return {config};
|
|
160
|
+
}
|