iobroker.eos-admin 7.9.93 → 7.9.95
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/INSTALL_TEST_V7.9.95.md +15 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +23 -18
- package/NEXOWATT_EOS_STABLE_7.9.94.md +16 -0
- package/{PUBLISH_STABLE_V7.9.93.md → PUBLISH_STABLE_V7.9.95.md} +2 -5
- package/README_STABILITY_V7.9.95.md +12 -0
- package/RELEASE_ACCEPTANCE_V7.9.95.md +11 -0
- package/RELEASE_NOTES_V7.9.95.md +12 -0
- package/adminWww/assets/Adapters-B5_jQ7DE-v84.js +1 -1
- package/adminWww/css/nexowatt-native-shell.css +4 -156
- package/adminWww/index.html +13 -11
- package/adminWww/js/eos-account-management.js +1 -1
- package/adminWww/js/eos-assistant.js +5 -556
- package/adminWww/js/eos-basic-settings.js +1 -1
- package/adminWww/js/eos-branding-sanitizer.js +1 -3
- package/adminWww/js/eos-native-security.js +1 -1
- package/adminWww/js/eos-policy-client.js +1 -1
- package/adminWww/js/eos-role-bootstrap.js +2 -2
- package/adminWww/js/eos-role-ui.js +1 -1
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/adminWww/static/js/nexowatt-stable-login.js +79 -0
- package/build/lib/chat/assistantKnowledge.js +6 -21
- package/build/lib/chat/chatOrchestrator.js +38 -33
- package/build/lib/chat/mcpClientManager.js +1 -1
- package/build/lib/nexowattStableAuth.js +74 -0
- package/build/lib/web.js +24 -7
- package/build/main.js +123 -308
- package/io-package.json +20 -16
- package/package.json +23 -32
- package/INSTALL_TEST_V7.9.93.md +0 -12
- package/README_STABILITY_V7.9.93.md +0 -15
- package/RELEASE_ACCEPTANCE_V7.9.93.md +0 -15
- package/RELEASE_NOTES_V7.9.93.md +0 -10
- package/tools/nexowatt-account-management-selftest.cjs +0 -61
- package/tools/nexowatt-clean-legacy-runtime.cjs +0 -147
- package/tools/nexowatt-ensure-release-defaults.cjs +0 -72
- package/tools/nexowatt-generate-eos-admin-repo-entry.cjs +0 -37
- package/tools/nexowatt-generate-repo-entry.cjs +0 -3
- package/tools/nexowatt-internal-reserve-selftest.cjs +0 -57
- package/tools/nexowatt-login-layout-selftest.cjs +0 -62
- package/tools/nexowatt-modern-ui-selftest.cjs +0 -50
- package/tools/nexowatt-patch-repo.cjs +0 -54
- package/tools/nexowatt-publish-channel-guard.cjs +0 -58
- package/tools/nexowatt-release-defaults-selftest.cjs +0 -29
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +0 -47
- package/tools/nexowatt-validate-package.cjs +0 -246
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* NexoWatt EOS stable runtime v94 */
|
|
2
|
+
(() => {
|
|
3
|
+
'use strict';
|
|
4
|
+
const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
|
|
5
|
+
const ASSIST_TEXT = /EOS\s*Assist/i;
|
|
6
|
+
const PASSWORDLESS_TEXT = /Erstanmeldung\s+ohne\s+Passwort|first\s+sign-?in\s+without\s+password/i;
|
|
7
|
+
const isLogin = () => /(?:\?|&)login(?:[=&]|$)/i.test(location.search) || /\/login(?:[/?#]|$)/i.test(location.pathname + location.hash);
|
|
8
|
+
const visible = el => !!el && el.getClientRects().length > 0;
|
|
9
|
+
const findInput = kind => {
|
|
10
|
+
const inputs = [...document.querySelectorAll('input')].filter(visible);
|
|
11
|
+
if (kind === 'password') return inputs.find(i => i.type === 'password');
|
|
12
|
+
return inputs.find(i => i.type !== 'password' && /user|login|name/i.test(`${i.name} ${i.id} ${i.autocomplete} ${i.getAttribute('aria-label')||''}`)) || inputs.find(i => i.type !== 'password');
|
|
13
|
+
};
|
|
14
|
+
const findSubmit = () => [...document.querySelectorAll('button,input[type="submit"]')].find(el => visible(el) && (/anmelden|login|sign in/i.test((el.textContent||el.value||'').trim()) || el.type === 'submit'));
|
|
15
|
+
const commonAncestor = (a,b) => {
|
|
16
|
+
if (!a || !b) return null;
|
|
17
|
+
const parents = new Set(); let n=a;
|
|
18
|
+
while(n){parents.add(n);n=n.parentElement;}
|
|
19
|
+
n=b; while(n&&!parents.has(n))n=n.parentElement;
|
|
20
|
+
return n;
|
|
21
|
+
};
|
|
22
|
+
const markLogin = () => {
|
|
23
|
+
if (!isLogin()) return;
|
|
24
|
+
document.documentElement.classList.add('nw-eos-login-v94');
|
|
25
|
+
document.body?.classList.add('nw-eos-login-v94');
|
|
26
|
+
const u=findInput('user'), p=findInput('password'), submit=findSubmit();
|
|
27
|
+
if (u && p) {
|
|
28
|
+
let card=commonAncestor(u,p);
|
|
29
|
+
while(card && card.parentElement && card.querySelectorAll('input').length < 2) card=card.parentElement;
|
|
30
|
+
if (card) {
|
|
31
|
+
card.setAttribute('data-nw-login-card','true');
|
|
32
|
+
let outer=card.parentElement;
|
|
33
|
+
for(let i=0;i<3 && outer?.parentElement;i++){
|
|
34
|
+
if(outer.getBoundingClientRect().width > card.getBoundingClientRect().width+20) break;
|
|
35
|
+
outer=outer.parentElement;
|
|
36
|
+
}
|
|
37
|
+
outer?.setAttribute('data-nw-login-shell','true');
|
|
38
|
+
}
|
|
39
|
+
const sync=()=>{
|
|
40
|
+
const name=(u.value||'').trim().toLowerCase();
|
|
41
|
+
const pass=p.value||'';
|
|
42
|
+
if(submit && MANAGED_USERS.has(name) && !pass){
|
|
43
|
+
submit.disabled=true;
|
|
44
|
+
submit.setAttribute('aria-disabled','true');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
if(!u.dataset.nwV94){u.dataset.nwV94='1';u.addEventListener('input',sync);p.addEventListener('input',sync);sync();}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const hideLegacyUi = () => {
|
|
51
|
+
for (const el of document.querySelectorAll('button,a,[role="button"],div,span')) {
|
|
52
|
+
const text=(el.textContent||'').trim();
|
|
53
|
+
if(!text || text.length>100) continue;
|
|
54
|
+
if(ASSIST_TEXT.test(text) || PASSWORDLESS_TEXT.test(text)){
|
|
55
|
+
const target=el.closest('button,a,[role="button"],[data-eos-assist],.eos-assist') || el;
|
|
56
|
+
target.style.setProperty('display','none','important');
|
|
57
|
+
target.setAttribute('aria-hidden','true');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const apply=()=>{markLogin();hideLegacyUi();};
|
|
62
|
+
const style=document.createElement('style');
|
|
63
|
+
style.id='nw-eos-stable-v94-css';
|
|
64
|
+
style.textContent=`
|
|
65
|
+
html.nw-eos-login-v94,body.nw-eos-login-v94{min-height:100%;overflow-x:hidden!important;overflow-y:auto!important}
|
|
66
|
+
.nw-eos-login-v94 [data-nw-login-shell="true"]{width:min(410px,calc(100vw - 28px))!important;min-height:540px!important;height:auto!important;max-height:none!important;overflow:visible!important;box-sizing:border-box!important;padding:22px!important}
|
|
67
|
+
.nw-eos-login-v94 [data-nw-login-card="true"]{width:min(354px,calc(100vw - 56px))!important;min-height:474px!important;height:auto!important;max-height:none!important;overflow:visible!important;box-sizing:border-box!important;padding-bottom:26px!important}
|
|
68
|
+
.nw-eos-login-v94 [data-nw-login-card="true"] *{scrollbar-width:none}
|
|
69
|
+
.nw-eos-login-v94 [data-nw-login-card="true"] *::-webkit-scrollbar{display:none!important;width:0!important;height:0!important}
|
|
70
|
+
.nw-eos-login-v94 [data-nw-login-card="true"] [role="alert"]{position:static!important;max-height:none!important;overflow:visible!important;white-space:normal!important}
|
|
71
|
+
[data-eos-assist],.eos-assist,.nexowatt-eos-assist{display:none!important}
|
|
72
|
+
@media(max-height:620px){.nw-eos-login-v94 [data-nw-login-shell="true"]{min-height:0!important;margin:16px auto!important}.nw-eos-login-v94 [data-nw-login-card="true"]{min-height:0!important}}
|
|
73
|
+
`;
|
|
74
|
+
(document.head||document.documentElement).appendChild(style);
|
|
75
|
+
const observer=new MutationObserver(()=>requestAnimationFrame(apply));
|
|
76
|
+
observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['class','style']});
|
|
77
|
+
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',apply,{once:true});else apply();
|
|
78
|
+
setInterval(apply,1500);
|
|
79
|
+
})();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Curated "wiki" of
|
|
3
|
+
* Curated "wiki" of ioBroker how-to knowledge that is injected into the assistant's system prompt.
|
|
4
4
|
*
|
|
5
5
|
* This is meant to grow over time: add a new {@link KnowledgeSection} to {@link KNOWLEDGE_SECTIONS}
|
|
6
6
|
* for each task the assistant should know how to guide the user through. Keep entries short and
|
|
@@ -10,21 +10,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
exports.KNOWLEDGE_SECTIONS = void 0;
|
|
11
11
|
exports.renderAssistantKnowledge = renderAssistantKnowledge;
|
|
12
12
|
exports.KNOWLEDGE_SECTIONS = [
|
|
13
|
-
{
|
|
14
|
-
title: 'NexoWatt EOS product areas and role-aware support',
|
|
15
|
-
body: [
|
|
16
|
-
'EOS Assist can answer questions about the live NexoWatt EOS system: Overview/Cockpit, Modules,',
|
|
17
|
-
'Services, Datapoints, Structure (rooms/functions), System logs, Access & Rights, NexoWatt EOS',
|
|
18
|
-
'applications, System hosts, Files and NexoWatt Backup. It can also inspect installed modules,',
|
|
19
|
-
'running services, object/state metadata, current values, device assignments, logs and history',
|
|
20
|
-
'through the available read tools. Always query real system data before making a system-specific claim.',
|
|
21
|
-
'Respect the authenticated role: Admin/Service may receive full system and service detail;',
|
|
22
|
-
'Installers may receive commissioning and troubleshooting detail available to their account;',
|
|
23
|
-
'End users receive only their permitted Smart Home, display and operating information.',
|
|
24
|
-
'Internal package names and compatibility identifiers are technical details; use NexoWatt EOS product',
|
|
25
|
-
'terminology in normal answers and show technical IDs only when they are required for a precise action.',
|
|
26
|
-
].join(' '),
|
|
27
|
-
},
|
|
28
13
|
{
|
|
29
14
|
title: 'Open an adapter instance configuration',
|
|
30
15
|
body: [
|
|
@@ -212,7 +197,7 @@ exports.KNOWLEDGE_SECTIONS = [
|
|
|
212
197
|
{
|
|
213
198
|
title: 'Run a script in the javascript adapter (run_javascript)',
|
|
214
199
|
body: [
|
|
215
|
-
'To run logic WITH the full
|
|
200
|
+
'To run logic WITH the full ioBroker scripting API, use run_javascript (act mode). It sends the',
|
|
216
201
|
'code to the `javascript` adapter and runs it ONCE (not saved), returning { ok, error, output,',
|
|
217
202
|
'logs }. Choose `language` "js" (default) or "ts". Inside the script the usual API is available:',
|
|
218
203
|
'`getState("id").val`, `setState("id", value)`, `on({id, change:"ne"}, …)`, `schedule(...)`,',
|
|
@@ -225,10 +210,10 @@ exports.KNOWLEDGE_SECTIONS = [
|
|
|
225
210
|
].join('\n'),
|
|
226
211
|
},
|
|
227
212
|
{
|
|
228
|
-
title: 'Automation options in
|
|
213
|
+
title: 'Automation options in ioBroker — pick the right tool',
|
|
229
214
|
body: [
|
|
230
|
-
'
|
|
231
|
-
'SEPARATE adapter that is NOT part of a default
|
|
215
|
+
'ioBroker can automate from simple to fully programmable. IMPORTANT: every option below needs a',
|
|
216
|
+
'SEPARATE adapter that is NOT part of a default ioBroker installation — `scenes`, `scheduler`,',
|
|
232
217
|
'`fullcalendar` and `javascript` (which provides Blockly, JavaScript and TypeScript). Before you',
|
|
233
218
|
'recommend or use one, CHECK whether it is installed (list_adapters, and list_instances for a',
|
|
234
219
|
'running instance). If it is missing, tell the user it must be installed first and offer',
|
|
@@ -286,7 +271,7 @@ exports.KNOWLEDGE_SECTIONS = [
|
|
|
286
271
|
].join('\n'),
|
|
287
272
|
},
|
|
288
273
|
{
|
|
289
|
-
title: '
|
|
274
|
+
title: 'ioBroker JavaScript adapter API (cheat-sheet for scripts and run_javascript)',
|
|
290
275
|
body: [
|
|
291
276
|
'When you WRITE a script (for the Scripts tab or for run_javascript), use the ioBroker javascript',
|
|
292
277
|
'API — it is NOT plain Node.js. Key functions:',
|
|
@@ -76,48 +76,53 @@ class ChatOrchestrator {
|
|
|
76
76
|
this.mcp = mcp;
|
|
77
77
|
this.adapter = adapter;
|
|
78
78
|
}
|
|
79
|
-
/** System prompt describing
|
|
80
|
-
buildSystemPrompt(language, mode, uiContext
|
|
81
|
-
const roleText = accessRole === 'admin'
|
|
82
|
-
? 'Admin / NexoWatt Service with full service visibility'
|
|
83
|
-
: accessRole === 'installer'
|
|
84
|
-
? 'Installer with commissioning and troubleshooting visibility'
|
|
85
|
-
: 'End user with only explicitly permitted Smart Home and operating visibility';
|
|
79
|
+
/** System prompt describing the assistant's role, tool usage and the current permission mode. */
|
|
80
|
+
buildSystemPrompt(language, mode, uiContext) {
|
|
86
81
|
const lines = [
|
|
87
|
-
'You are
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
82
|
+
'You are the ioBroker Admin Assistant, an in-app helper inside the ioBroker admin web UI.',
|
|
83
|
+
'You help the user operate their ioBroker installation: explain how to do things in the UI,',
|
|
84
|
+
'recommend which adapter to use for a device or service, and answer questions about the',
|
|
85
|
+
"user's actual system (their states, objects, devices grouped by room/function, installed",
|
|
86
|
+
'adapters, running instances, logs and historical values).',
|
|
92
87
|
'',
|
|
93
|
-
|
|
94
|
-
'
|
|
95
|
-
'
|
|
88
|
+
'KEY CAPABILITY: you can navigate the admin UI yourself. When the user asks you to OPEN or GO to',
|
|
89
|
+
'something, call the navigate_admin_ui tool with the hash route — it opens the tab OR the specific',
|
|
90
|
+
'dialog immediately (no confirmation, it only changes the view), e.g.',
|
|
91
|
+
'{"hash":"#tab-hosts/base-settings/system.host.MSI"}. Otherwise (just pointing the user somewhere)',
|
|
92
|
+
'offer a clickable Markdown hash link, e.g. [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI).',
|
|
93
|
+
'NEVER say a dialog can only be opened manually or that deep-linking a dialog is not possible.',
|
|
96
94
|
'',
|
|
97
|
-
'
|
|
98
|
-
'-
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'-
|
|
104
|
-
'-
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
|
|
95
|
+
'Guidelines:',
|
|
96
|
+
'- Use the available tools to look up real data from THIS ioBroker system instead of guessing.',
|
|
97
|
+
' E.g. to list devices in a room call list_devices with the room name; to read a value call',
|
|
98
|
+
' get_states; to find objects call search_objects.',
|
|
99
|
+
'- Base your answer on the tool results. If a tool returns an error or nothing, say so briefly',
|
|
100
|
+
' and suggest what the user could check.',
|
|
101
|
+
'- Be concise and practical. Use short lists or tables when listing devices or states.',
|
|
102
|
+
'- Object IDs are case-sensitive (e.g. "hue.0.lights.1.on"); show them in `monospace`.',
|
|
103
|
+
'- IMPORTANT — this admin version SUPPORTS deep links that open specific DIALOGS directly,',
|
|
104
|
+
' not just tabs. It is FALSE to say a dialog "can only be opened with the gear/cog icon" or',
|
|
105
|
+
' that deep-linking a dialog is "technically not possible" — never say that. Instead always',
|
|
106
|
+
' offer a clickable Markdown hash link and let the USER click it; do NOT claim you opened or',
|
|
107
|
+
' navigated anywhere yourself. Examples: [Instances](#tab-instances), [Objects](#tab-objects),',
|
|
108
|
+
' [hue.0 config](#tab-instances/config/hue.0), [edit this object](#tab-objects/edit/<id>),',
|
|
109
|
+
" and a host's base-settings dialog, e.g. MSI:",
|
|
110
|
+
' [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI). The exact dialog routes are',
|
|
111
|
+
' listed in the knowledge below. Prefer these links over the navigate_admin_ui tool.',
|
|
112
|
+
`- Answer in the user's language. The admin UI language is currently "${language}".`,
|
|
110
113
|
];
|
|
111
114
|
if (mode === 'act') {
|
|
112
|
-
lines.push('-
|
|
115
|
+
lines.push('- You may change things using the write/action tools (set_state, create_user_state,', ' extend_object, assign_to_enums, install_adapter, run_command, run_node_script). Every such', ' call is shown to the user for explicit confirmation before it runs, so propose the smallest', ' sensible action.', '- create_user_state only works in "0_userdata.0." or "javascript.<n>." — never elsewhere.', '- extend_object merges common/native into an existing object — use it to start/stop an', ' instance (common.enabled), change instance settings (native) or set enum members.', '- assign_to_enums adds objects to room/function enums (add-only) — use it to sort devices.', '- run_command runs an ioBroker CLI command and streams its output to the user; use it for', ' maintenance actions (restart, upload, fix, ...), not to fetch data.', '- run_node_script executes a short Node.js script on the host to CHECK things (network, files,', ' environment). Prefer read-only checks, keep it short, and put results in console.log. Never', ' use it for destructive operations.', '- run_javascript runs a one-off JS/TS script INSIDE the javascript adapter (full ioBroker API:', ' on/getState/setState/schedule/sendTo). Use it to test automation logic or do an API-based', ' check/action; it is not saved. Use run_node_script instead for plain OS checks (no ioBroker API).');
|
|
113
116
|
}
|
|
114
117
|
else {
|
|
115
|
-
lines.push('-
|
|
118
|
+
lines.push('- You currently have READ-ONLY access: you cannot change states, create objects or install', ' adapters. When a change is needed, explain the exact steps the user should take in the UI.');
|
|
116
119
|
}
|
|
117
120
|
if (uiContext?.hash) {
|
|
118
|
-
lines.push('', `The user is currently
|
|
121
|
+
lines.push('', `The user is currently in the admin UI at the route \`${uiContext.hash}\`. Take this into ` +
|
|
122
|
+
"account — don't tell them to open a tab/page they are already on, and refer to where " +
|
|
123
|
+
'they are when helpful.');
|
|
119
124
|
}
|
|
120
|
-
lines.push('', '
|
|
125
|
+
lines.push('', 'ioBroker how-to knowledge (use this to guide the user precisely):', (0, assistantKnowledge_1.renderAssistantKnowledge)());
|
|
121
126
|
return { role: 'system', content: lines.join('\n') };
|
|
122
127
|
}
|
|
123
128
|
/** Assemble the tool list offered to the model for the given mode. */
|
|
@@ -149,7 +154,7 @@ class ChatOrchestrator {
|
|
|
149
154
|
? { ...message, content: parseTextToolCalls(message.content).content }
|
|
150
155
|
: message);
|
|
151
156
|
if (!messages.some(message => message.role === 'system')) {
|
|
152
|
-
messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext
|
|
157
|
+
messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext));
|
|
153
158
|
}
|
|
154
159
|
const newMessages = [];
|
|
155
160
|
const steps = [];
|
|
@@ -44,7 +44,7 @@ class McpClientManager {
|
|
|
44
44
|
language: this.options.language,
|
|
45
45
|
allowSetState: this.options.allowSetState ?? false,
|
|
46
46
|
allowObjectChange: this.options.allowObjectChange ?? false,
|
|
47
|
-
clientName: '
|
|
47
|
+
clientName: 'ioBroker.admin chat helper',
|
|
48
48
|
clientVersion: '1.0.0',
|
|
49
49
|
})
|
|
50
50
|
.then(mcp => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startNexowattStableAuth = startNexowattStableAuth;
|
|
4
|
+
const START_PASSWORD = 'nexowatt';
|
|
5
|
+
const MARKER_KEYS = ["eosFirstLoginPending", "eosMustChangePassword", "eosPasswordSetupRequired", "firstLoginRequired", "mustChangePassword", "nexowattFirstLoginPending", "nexowattMustChangePassword", "nexowattPasswordSetupRequired", "passwordChangeRequired"];
|
|
6
|
+
const timers = new WeakMap();
|
|
7
|
+
function pendingByNative(native) {
|
|
8
|
+
if (!native || typeof native !== 'object') return false;
|
|
9
|
+
return MARKER_KEYS.some(key => native[key] === true || native[key] === 'pending' || native[key] === 'required' || native[key] === 'starter');
|
|
10
|
+
}
|
|
11
|
+
async function setPassword(adapter, user, password) {
|
|
12
|
+
if (typeof adapter.setPasswordAsync === 'function') return adapter.setPasswordAsync(user, password, { user: 'system.user.admin' });
|
|
13
|
+
return new Promise((resolve, reject) => adapter.setPassword(user, password, { user: 'system.user.admin' }, err => err ? reject(err) : resolve()));
|
|
14
|
+
}
|
|
15
|
+
async function ensureGroupMembership(adapter, userId, groupId) {
|
|
16
|
+
const group = await adapter.getForeignObjectAsync(groupId);
|
|
17
|
+
if (!group || group.type !== 'group') return;
|
|
18
|
+
group.common = group.common || { name: groupId.split('.').pop(), members: [] };
|
|
19
|
+
group.common.members = Array.isArray(group.common.members) ? group.common.members : [];
|
|
20
|
+
if (!group.common.members.includes(userId)) {
|
|
21
|
+
group.common.members.push(userId);
|
|
22
|
+
await adapter.setForeignObjectAsync(groupId, group, { user: 'system.user.admin' });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function createUser(adapter, name) {
|
|
26
|
+
const id = `system.user.${name}`;
|
|
27
|
+
await adapter.setForeignObjectAsync(id, {
|
|
28
|
+
_id: id, type: 'user',
|
|
29
|
+
common: { name, enabled: true, password: '' },
|
|
30
|
+
native: { nexowattManagedAccount: true, nexowattMustChangePassword: true, nexowattPasswordState: 'starter' }
|
|
31
|
+
}, { user: 'system.user.admin' });
|
|
32
|
+
return adapter.getForeignObjectAsync(id);
|
|
33
|
+
}
|
|
34
|
+
async function prepareOne(adapter, name, group, createMissing) {
|
|
35
|
+
const id = `system.user.${name}`;
|
|
36
|
+
let obj = await adapter.getForeignObjectAsync(id);
|
|
37
|
+
let created = false;
|
|
38
|
+
if (!obj && createMissing) { obj = await createUser(adapter, name); created = true; }
|
|
39
|
+
if (!obj || obj.type !== 'user') return;
|
|
40
|
+
obj.native = obj.native || {};
|
|
41
|
+
const empty = !obj.common || !obj.common.password;
|
|
42
|
+
const pending = created || empty || pendingByNative(obj.native) || obj.native.nexowattPasswordState === 'starter';
|
|
43
|
+
if (pending) {
|
|
44
|
+
await setPassword(adapter, name, START_PASSWORD);
|
|
45
|
+
obj = await adapter.getForeignObjectAsync(id) || obj;
|
|
46
|
+
obj.native = obj.native || {};
|
|
47
|
+
obj.native.nexowattManagedAccount = true;
|
|
48
|
+
obj.native.nexowattStarterPasswordActive = true;
|
|
49
|
+
obj.native.nexowattMustChangePassword = true;
|
|
50
|
+
obj.native.nexowattPasswordState = 'starter';
|
|
51
|
+
obj.native.nexowattStarterPasswordUpdatedAt = new Date().toISOString();
|
|
52
|
+
for (const key of MARKER_KEYS) {
|
|
53
|
+
if (/required|must|pending|first/i.test(key)) obj.native[key] = true;
|
|
54
|
+
}
|
|
55
|
+
await adapter.setForeignObjectAsync(id, obj, { user: 'system.user.admin' });
|
|
56
|
+
}
|
|
57
|
+
await ensureGroupMembership(adapter, id, group);
|
|
58
|
+
}
|
|
59
|
+
async function reconcile(adapter) {
|
|
60
|
+
try {
|
|
61
|
+
await prepareOne(adapter, 'installer', 'system.group.installer', true);
|
|
62
|
+
const guest = await adapter.getForeignObjectAsync('system.user.guest');
|
|
63
|
+
const user = await adapter.getForeignObjectAsync('system.user.user');
|
|
64
|
+
if (guest || !user) await prepareOne(adapter, 'guest', 'system.group.endkunden', true);
|
|
65
|
+
if (user) await prepareOne(adapter, 'user', 'system.group.endkunden', false);
|
|
66
|
+
} catch (e) { adapter.log.warn(`[NexoWatt EOS login] Starter-account reconciliation failed: ${e && e.message || e}`); }
|
|
67
|
+
}
|
|
68
|
+
function startNexowattStableAuth(adapter) {
|
|
69
|
+
if (timers.has(adapter)) return;
|
|
70
|
+
void reconcile(adapter);
|
|
71
|
+
const timer = setInterval(() => void reconcile(adapter), 5000);
|
|
72
|
+
if (timer.unref) timer.unref();
|
|
73
|
+
timers.set(adapter, timer);
|
|
74
|
+
}
|
package/build/lib/web.js
CHANGED
|
@@ -675,7 +675,11 @@ class Web {
|
|
|
675
675
|
const account = (native.nexowattEosAccount || {});
|
|
676
676
|
const initialized = account.passwordInitialized === true
|
|
677
677
|
|| Number(account.passwordSetupVersion || account.passwordInitializationVersion || 0) >= 1;
|
|
678
|
-
const
|
|
678
|
+
const stableForced = native.nexowattPasswordChangeRequired === true
|
|
679
|
+
|| native.eosPasswordChangeRequired === true
|
|
680
|
+
|| native.nexowattFirstLoginPending === true
|
|
681
|
+
|| native.eosFirstLoginRequired === true;
|
|
682
|
+
const forced = account.forcePasswordChange === true || stableForced;
|
|
679
683
|
return { required: forced || !initialized, initialized: initialized && !forced, minLength, version: 1 };
|
|
680
684
|
}
|
|
681
685
|
catch (e) {
|
|
@@ -789,6 +793,12 @@ class Web {
|
|
|
789
793
|
account.firstLoginCompletedAt = now;
|
|
790
794
|
account.forcePasswordChange = false;
|
|
791
795
|
account.passwordlessFirstLoginAllowed = false;
|
|
796
|
+
native.nexowattPasswordChangeRequired = false;
|
|
797
|
+
native.eosPasswordChangeRequired = false;
|
|
798
|
+
native.nexowattFirstLoginPending = false;
|
|
799
|
+
native.eosFirstLoginRequired = false;
|
|
800
|
+
native.nexowattInitialPasswordApplied = false;
|
|
801
|
+
native.nexowattPasswordChangedAt = now;
|
|
792
802
|
await this.adapter.setForeignObjectAsync(access.userId, userObject);
|
|
793
803
|
await this.destroyEosRequestSessions(req);
|
|
794
804
|
for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
|
|
@@ -1133,8 +1143,7 @@ class Web {
|
|
|
1133
1143
|
res.status(409).json({ error: 'accountDisabled' });
|
|
1134
1144
|
return;
|
|
1135
1145
|
}
|
|
1136
|
-
|
|
1137
|
-
await this.setEosUserPassword(targetUserId, bootstrapSecret);
|
|
1146
|
+
await this.setEosUserPassword(targetUserId, 'nexowatt');
|
|
1138
1147
|
const user = (await this.adapter.getForeignObjectAsync(targetUserId));
|
|
1139
1148
|
if (!user) {
|
|
1140
1149
|
throw new Error('userObjectUnavailableAfterPasswordReset');
|
|
@@ -1146,21 +1155,29 @@ class Web {
|
|
|
1146
1155
|
account.passwordSetupVersion = 0;
|
|
1147
1156
|
account.passwordInitializationVersion = 0;
|
|
1148
1157
|
account.forcePasswordChange = true;
|
|
1149
|
-
account.passwordlessFirstLoginAllowed =
|
|
1158
|
+
account.passwordlessFirstLoginAllowed = false;
|
|
1150
1159
|
account.passwordResetAt = now;
|
|
1151
1160
|
account.passwordResetBy = access.userId;
|
|
1152
|
-
account.passwordResetMode = '
|
|
1161
|
+
account.passwordResetMode = 'initial-password';
|
|
1162
|
+
native.nexowattPasswordChangeRequired = true;
|
|
1163
|
+
native.eosPasswordChangeRequired = true;
|
|
1164
|
+
native.nexowattFirstLoginPending = true;
|
|
1165
|
+
native.eosFirstLoginRequired = true;
|
|
1166
|
+
native.nexowattInitialPasswordApplied = true;
|
|
1167
|
+
native.nexowattInitialPasswordVersion = 1;
|
|
1168
|
+
native.nexowattStableInitialCredentialVersion = 1;
|
|
1153
1169
|
delete account.passwordSetAt;
|
|
1154
1170
|
delete account.firstLoginCompletedAt;
|
|
1155
1171
|
await this.adapter.setForeignObjectAsync(targetUserId, user);
|
|
1156
1172
|
this.invalidateEosPasswordClaimsForUser(targetUserId);
|
|
1157
|
-
this.adapter.log.warn(`EOS account ${targetUserId} was reset
|
|
1173
|
+
this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
|
|
1158
1174
|
res.status(200).json({
|
|
1159
1175
|
success: true,
|
|
1160
1176
|
user: targetUserId,
|
|
1161
1177
|
role: explicitlyManagedRole,
|
|
1162
1178
|
firstLoginRequired: true,
|
|
1163
|
-
passwordlessFirstLoginAllowed:
|
|
1179
|
+
passwordlessFirstLoginAllowed: false,
|
|
1180
|
+
initialPasswordRequired: true,
|
|
1164
1181
|
});
|
|
1165
1182
|
}
|
|
1166
1183
|
async getEosHistoryInstances() {
|