iobroker.eos-admin 7.10.4 → 7.10.6
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.10.6.md +8 -0
- package/MERGE_UPDATE_README_V7.10.6.md +7 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +19 -18
- package/NEXOWATT_EOS_PREBUILT_MANIFEST.json +40 -41
- package/PUBLISH_STABLE_V7.10.6.md +8 -0
- package/README_STABILITY_V7.10.6.md +5 -0
- package/RELEASE_ACCEPTANCE_V7.10.6.md +11 -0
- package/RELEASE_NOTES_V7.10.6.md +14 -0
- package/SECURITY_ROLE_MATRIX_V7.10.6.md +12 -0
- package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
- package/adminWww/assets/Objects-DPan0bzw-v84.js +1 -1
- package/adminWww/assets/bootstrap-COulQZax-v84.js +2 -2
- package/adminWww/css/nexowatt-native-shell.css +12 -1
- package/adminWww/index.html +14 -15
- package/adminWww/js/eos-role-bootstrap.js +5 -5
- package/adminWww/js/eos-role-ui.js +211 -17
- package/adminWww/js/nexowatt-native-shell.js +1 -1
- package/adminWww/static/js/{nexowatt-stable-v7101.js → nexowatt-stable-v7106.js} +1 -1
- package/build/lib/web.js +34 -0
- package/build/main.js +13 -15
- package/io-package.json +32 -5
- package/package.json +11 -10
- package/tools/nexowatt-account-password-flow-selftest.cjs +3 -3
- package/tools/nexowatt-clean-legacy-runtime.cjs +6 -0
- package/tools/nexowatt-internal-reserve-selftest.cjs +1 -1
- package/tools/nexowatt-login-layout-selftest.cjs +3 -3
- package/tools/nexowatt-merge-update-selftest.cjs +1 -1
- package/tools/nexowatt-modern-ui-selftest.cjs +2 -2
- package/tools/nexowatt-role-security-selftest.cjs +147 -41
- package/tools/nexowatt-runtime-cleanup-selftest.cjs +4 -0
- package/tools/nexowatt-stable-v7103-selftest.cjs +4 -4
- package/tools/nexowatt-ui-overview-runtime-selftest.cjs +2 -2
- package/tools/nexowatt-user-area-selftest.cjs +36 -0
- package/tools/nexowatt-validate-package.cjs +9 -8
- package/INSTALL_TEST_V7.10.4.md +0 -9
- package/MERGE_UPDATE_README_V7.10.4.md +0 -3
- package/PUBLISH_STABLE_V7.10.4.md +0 -10
- package/README_STABILITY_V7.10.4.md +0 -12
- package/RELEASE_ACCEPTANCE_V7.10.4.md +0 -3
- package/RELEASE_NOTES_V7.10.4.md +0 -11
- package/adminWww/nexowatt-role-security.js +0 -336
- package/build/lib/eosRoleSecurity.js +0 -310
- package/src-admin/README.md +0 -69
- package/src-admin/nexowatt-role-security.js +0 -336
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* NexoWatt EOS role security boundary.
|
|
5
|
-
*
|
|
6
|
-
* This module intentionally uses only Node.js core APIs so the prebuilt
|
|
7
|
-
* repository can be packed and published without a local node_modules folder.
|
|
8
|
-
*/
|
|
9
|
-
const http = require("node:http");
|
|
10
|
-
const https = require("node:https");
|
|
11
|
-
const { URL } = require("node:url");
|
|
12
|
-
|
|
13
|
-
let adapterRef = null;
|
|
14
|
-
let installed = false;
|
|
15
|
-
let backupCache = { value: false, expires: 0 };
|
|
16
|
-
|
|
17
|
-
const ADMIN_NAMES = new Set(["admin", "administrator", "system.user.admin"]);
|
|
18
|
-
const INSTALLER_TOKENS = ["installer", "installateur", "inbetriebnahme"];
|
|
19
|
-
const CUSTOMER_TOKENS = ["user", "guest", "gast", "endkunde", "customer", "smarthome"];
|
|
20
|
-
|
|
21
|
-
const ADMIN_ONLY_PATHS = [
|
|
22
|
-
/(?:^|[\\/#?&_.-])app[\\s_.-]*center(?:$|[\\/#?&_.-])/i,
|
|
23
|
-
/(?:^|[\\/#?&_.-])lizenz(?:en)?(?:$|[\\/#?&_.-])/i,
|
|
24
|
-
/(?:^|[\\/#?&_.-])licen[cs]e(?:s)?(?:$|[\\/#?&_.-])/i,
|
|
25
|
-
/(?:^|[\\/#?&_.-])simulation(?:$|[\\/#?&_.-])/i,
|
|
26
|
-
/(?:^|[\\/#?&_.-])simulator(?:$|[\\/#?&_.-])/i,
|
|
27
|
-
/(?:^|[\\/#?&_.-])experten?(?:modus)?(?:$|[\\/#?&_.-])/i,
|
|
28
|
-
/(?:^|[\\/#?&_.-])expert(?:mode)?(?:$|[\\/#?&_.-])/i,
|
|
29
|
-
/(?:^|[\\/#?&_.-])xterm(?:$|[\\/#?&_.-])/i,
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const RIGHTS_PATHS = [
|
|
33
|
-
/(?:^|[\\/#?&_.-])users?(?:$|[\\/#?&_.-])/i,
|
|
34
|
-
/(?:^|[\\/#?&_.-])groups?(?:$|[\\/#?&_.-])/i,
|
|
35
|
-
/zug[aä]nge/i,
|
|
36
|
-
/rechte/i,
|
|
37
|
-
/permissions?/i,
|
|
38
|
-
/password/i,
|
|
39
|
-
/passwort/i,
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
function normalize(value) {
|
|
43
|
-
if (value === null || value === undefined) return "";
|
|
44
|
-
if (typeof value === "object") {
|
|
45
|
-
if (typeof value.name === "string") return value.name.trim().toLowerCase();
|
|
46
|
-
if (typeof value.user === "string") return value.user.trim().toLowerCase();
|
|
47
|
-
return "";
|
|
48
|
-
}
|
|
49
|
-
return String(value).trim().toLowerCase();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function classifyRole(value) {
|
|
53
|
-
const text = normalize(value);
|
|
54
|
-
if (!text) return "unknown";
|
|
55
|
-
if (ADMIN_NAMES.has(text) || /(?:^|[.:/_-])admin(?:istrator)?(?:$|[.:/_-])/.test(text)) return "admin";
|
|
56
|
-
if (INSTALLER_TOKENS.some(token => text.includes(token))) return "installer";
|
|
57
|
-
if (CUSTOMER_TOKENS.some(token => text === token || text.includes(token))) return "customer";
|
|
58
|
-
return "unknown";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseBasicUser(header) {
|
|
62
|
-
if (!header || !/^basic\s+/i.test(header)) return "";
|
|
63
|
-
try {
|
|
64
|
-
const decoded = Buffer.from(header.replace(/^basic\s+/i, ""), "base64").toString("utf8");
|
|
65
|
-
return decoded.split(":", 1)[0] || "";
|
|
66
|
-
} catch {
|
|
67
|
-
return "";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseCookieHeader(header) {
|
|
72
|
-
const out = Object.create(null);
|
|
73
|
-
String(header || "").split(";").forEach(part => {
|
|
74
|
-
const idx = part.indexOf("=");
|
|
75
|
-
if (idx <= 0) return;
|
|
76
|
-
const key = part.slice(0, idx).trim();
|
|
77
|
-
let value = part.slice(idx + 1).trim();
|
|
78
|
-
try { value = decodeURIComponent(value); } catch { /* ignore */ }
|
|
79
|
-
out[key] = value;
|
|
80
|
-
});
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function extractIdentity(req) {
|
|
85
|
-
const candidates = [];
|
|
86
|
-
if (req) {
|
|
87
|
-
candidates.push(req.user, req.username, req.auth && req.auth.user, req.session && req.session.user,
|
|
88
|
-
req.session && req.session.username, req.session && req.session.passport && req.session.passport.user,
|
|
89
|
-
req.ioBrokerUser, req._user);
|
|
90
|
-
const headers = req.headers || {};
|
|
91
|
-
candidates.push(headers["x-iobroker-user"], headers["x-eos-user"], headers["x-authenticated-user"],
|
|
92
|
-
headers["x-forwarded-user"], parseBasicUser(headers.authorization));
|
|
93
|
-
const cookies = parseCookieHeader(headers.cookie);
|
|
94
|
-
for (const key of ["iobroker.user", "iobroker_user", "eos.user", "eosUser", "user", "username", "authUser"]) {
|
|
95
|
-
candidates.push(cookies[key]);
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const parsed = new URL(req.url || "/", "http://127.0.0.1");
|
|
99
|
-
candidates.push(parsed.searchParams.get("user"), parsed.searchParams.get("username"));
|
|
100
|
-
} catch { /* ignore */ }
|
|
101
|
-
}
|
|
102
|
-
for (const value of candidates) {
|
|
103
|
-
if (normalize(value)) return normalize(value);
|
|
104
|
-
}
|
|
105
|
-
return "";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function roleFromRequest(req) {
|
|
109
|
-
const explicitRole = normalize(req && req.headers && (req.headers["x-eos-role"] || req.headers["x-nexowatt-role"]));
|
|
110
|
-
const role = classifyRole(explicitRole);
|
|
111
|
-
if (role !== "unknown") return role;
|
|
112
|
-
return classifyRole(extractIdentity(req));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isAdminOnlyPath(rawUrl) {
|
|
116
|
-
const value = String(rawUrl || "");
|
|
117
|
-
return ADMIN_ONLY_PATHS.some(pattern => pattern.test(value));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isRightsPath(rawUrl) {
|
|
121
|
-
const value = String(rawUrl || "");
|
|
122
|
-
return RIGHTS_PATHS.some(pattern => pattern.test(value));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isStateWriteRequest(req) {
|
|
126
|
-
const method = String(req && req.method || "GET").toUpperCase();
|
|
127
|
-
if (!["POST", "PUT", "PATCH", "DELETE"].includes(method)) return false;
|
|
128
|
-
const value = String(req && req.url || "");
|
|
129
|
-
return /(?:setstate|write|objects?\/|states?\/|sendto|command)/i.test(value);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function deny(res, message, statusCode = 403) {
|
|
133
|
-
if (!res || res.headersSent || res.writableEnded) return true;
|
|
134
|
-
const accepts = String((res.req && res.req.headers && res.req.headers.accept) || "");
|
|
135
|
-
const json = accepts.includes("application/json") || String(res.req && res.req.url || "").includes("/api/");
|
|
136
|
-
res.statusCode = statusCode;
|
|
137
|
-
res.setHeader("Cache-Control", "no-store");
|
|
138
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
139
|
-
if (json) {
|
|
140
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
141
|
-
res.end(JSON.stringify({ ok: false, error: "forbidden", message }));
|
|
142
|
-
} else {
|
|
143
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
144
|
-
res.end(`<!doctype html><html lang="de"><meta charset="utf-8"><title>Zugriff gesperrt</title><style>body{margin:0;background:#031724;color:#eaf6ff;font:16px Arial,sans-serif;display:grid;place-items:center;min-height:100vh}.box{max-width:620px;padding:32px;border:1px solid #0dbb87;border-radius:18px;background:#071f2f}h1{margin-top:0;color:#24e6ad}</style><div class="box"><h1>Zugriff gesperrt</h1><p>${message}</p></div></html>`);
|
|
145
|
-
}
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function getBackupActive() {
|
|
150
|
-
const now = Date.now();
|
|
151
|
-
if (backupCache.expires > now) return backupCache.value;
|
|
152
|
-
let active = false;
|
|
153
|
-
const adapter = adapterRef;
|
|
154
|
-
if (adapter) {
|
|
155
|
-
try {
|
|
156
|
-
let rows = [];
|
|
157
|
-
if (typeof adapter.getObjectViewAsync === "function") {
|
|
158
|
-
const result = await adapter.getObjectViewAsync("system", "instance", { startkey: "system.adapter.", endkey: "system.adapter.\u9999" });
|
|
159
|
-
rows = result && Array.isArray(result.rows) ? result.rows : [];
|
|
160
|
-
} else if (typeof adapter.getObjectListAsync === "function") {
|
|
161
|
-
const result = await adapter.getObjectListAsync({ startkey: "system.adapter.", endkey: "system.adapter.\u9999" });
|
|
162
|
-
rows = result && Array.isArray(result.rows) ? result.rows : [];
|
|
163
|
-
}
|
|
164
|
-
active = rows.some(row => {
|
|
165
|
-
const obj = row && (row.value || row.doc);
|
|
166
|
-
const id = normalize(row && row.id || obj && obj._id);
|
|
167
|
-
return /system\.adapter\.(?:backitup|backup|nexowatt-backup|eos-backup)\./.test(id) && Boolean(obj && obj.common && obj.common.enabled);
|
|
168
|
-
});
|
|
169
|
-
} catch (error) {
|
|
170
|
-
if (adapter && adapter.log && typeof adapter.log.debug === "function") {
|
|
171
|
-
adapter.log.debug(`[role-security] Backup status could not be read: ${error && error.message || error}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
backupCache = { value: active, expires: now + 5000 };
|
|
176
|
-
return active;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function attachAdapter(adapter) {
|
|
180
|
-
adapterRef = adapter || null;
|
|
181
|
-
backupCache.expires = 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function cloneAclSection(section) {
|
|
185
|
-
return section && typeof section === "object" ? { ...section } : {};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function enforceRoleAcls(adapter = adapterRef) {
|
|
189
|
-
if (!adapter || typeof adapter.getForeignObjectAsync !== "function" || typeof adapter.setForeignObjectAsync !== "function") return;
|
|
190
|
-
const ids = ["system.group.endkunde"];
|
|
191
|
-
for (const id of ids) {
|
|
192
|
-
try {
|
|
193
|
-
const obj = await adapter.getForeignObjectAsync(id);
|
|
194
|
-
if (!obj || !obj.common) continue;
|
|
195
|
-
const acl = obj.common.acl && typeof obj.common.acl === "object" ? { ...obj.common.acl } : {};
|
|
196
|
-
const objectAcl = cloneAclSection(acl.object);
|
|
197
|
-
const stateAcl = cloneAclSection(acl.state);
|
|
198
|
-
const usersAcl = cloneAclSection(acl.users);
|
|
199
|
-
const fileAcl = cloneAclSection(acl.file);
|
|
200
|
-
const otherAcl = cloneAclSection(acl.other);
|
|
201
|
-
const before = JSON.stringify({ objectAcl, stateAcl, usersAcl, fileAcl, otherAcl });
|
|
202
|
-
Object.assign(objectAcl, { list: true, read: true, write: false, delete: false });
|
|
203
|
-
Object.assign(stateAcl, { list: true, read: true, write: false, delete: false, create: false });
|
|
204
|
-
Object.assign(usersAcl, { write: false, delete: false, create: false });
|
|
205
|
-
Object.assign(fileAcl, { list: true, read: true, write: false, delete: false, create: false });
|
|
206
|
-
Object.assign(otherAcl, { execute: false, sendto: false });
|
|
207
|
-
acl.object = objectAcl;
|
|
208
|
-
acl.state = stateAcl;
|
|
209
|
-
acl.users = usersAcl;
|
|
210
|
-
acl.file = fileAcl;
|
|
211
|
-
acl.other = otherAcl;
|
|
212
|
-
const after = JSON.stringify({ objectAcl, stateAcl, usersAcl, fileAcl, otherAcl });
|
|
213
|
-
if (before !== after) {
|
|
214
|
-
obj.common.acl = acl;
|
|
215
|
-
await adapter.setForeignObjectAsync(id, obj);
|
|
216
|
-
if (adapter.log && typeof adapter.log.info === "function") {
|
|
217
|
-
adapter.log.info("[role-security] End customer group enforced as read-only for objects and states");
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch (error) {
|
|
221
|
-
if (adapter.log && typeof adapter.log.warn === "function") {
|
|
222
|
-
adapter.log.warn(`[role-security] ACL enforcement failed for ${id}: ${error && error.message || error}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function handleStatus(req, res) {
|
|
229
|
-
const role = roleFromRequest(req);
|
|
230
|
-
const backupActive = await getBackupActive();
|
|
231
|
-
res.statusCode = 200;
|
|
232
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
233
|
-
res.setHeader("Cache-Control", "no-store");
|
|
234
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
235
|
-
res.end(JSON.stringify({
|
|
236
|
-
ok: true,
|
|
237
|
-
role,
|
|
238
|
-
user: extractIdentity(req) || null,
|
|
239
|
-
backupActive,
|
|
240
|
-
capabilities: {
|
|
241
|
-
appCenter: role === "admin",
|
|
242
|
-
license: role === "admin",
|
|
243
|
-
simulation: role === "admin",
|
|
244
|
-
expertMode: role === "admin",
|
|
245
|
-
accessRights: role === "admin",
|
|
246
|
-
xterm: role === "admin",
|
|
247
|
-
ioBrokerAdmin: role === "admin",
|
|
248
|
-
writeDatapoints: role === "admin" || role === "installer",
|
|
249
|
-
readDatapoints: true,
|
|
250
|
-
},
|
|
251
|
-
}));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function wrapListener(listener) {
|
|
255
|
-
if (typeof listener !== "function") return listener;
|
|
256
|
-
return function guardedListener(req, res) {
|
|
257
|
-
try {
|
|
258
|
-
const url = String(req && req.url || "/");
|
|
259
|
-
if (/^\/api\/nexowatt-eos\/security\/(?:status|me)(?:[/?#]|$)/i.test(url)) {
|
|
260
|
-
void handleStatus(req, res);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const role = roleFromRequest(req);
|
|
264
|
-
if (role !== "unknown" && role !== "admin" && (isAdminOnlyPath(url) || isRightsPath(url))) {
|
|
265
|
-
deny(res, "Diese Funktion ist ausschließlich für Administratoren freigegeben.");
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (role === "customer" && isStateWriteRequest(req)) {
|
|
269
|
-
deny(res, "Endkunden besitzen für Datenpunkte ausschließlich Leserechte.");
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
// Fail closed only for positively identified restricted users. Other
|
|
274
|
-
// requests continue to the existing server so startup is never blocked.
|
|
275
|
-
}
|
|
276
|
-
return listener.apply(this, arguments);
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function patchCreateServer(module) {
|
|
281
|
-
const original = module.createServer;
|
|
282
|
-
if (typeof original !== "function" || original.__nexowattRoleSecurity) return;
|
|
283
|
-
function secureCreateServer(...args) {
|
|
284
|
-
if (typeof args[0] === "function") args[0] = wrapListener(args[0]);
|
|
285
|
-
else if (typeof args[1] === "function") args[1] = wrapListener(args[1]);
|
|
286
|
-
return original.apply(this, args);
|
|
287
|
-
}
|
|
288
|
-
Object.defineProperty(secureCreateServer, "__nexowattRoleSecurity", { value: true });
|
|
289
|
-
module.createServer = secureCreateServer;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function installHttpGuard() {
|
|
293
|
-
if (installed) return;
|
|
294
|
-
installed = true;
|
|
295
|
-
patchCreateServer(http);
|
|
296
|
-
patchCreateServer(https);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
module.exports = {
|
|
300
|
-
attachAdapter,
|
|
301
|
-
classifyRole,
|
|
302
|
-
enforceRoleAcls,
|
|
303
|
-
extractIdentity,
|
|
304
|
-
getBackupActive,
|
|
305
|
-
installHttpGuard,
|
|
306
|
-
isAdminOnlyPath,
|
|
307
|
-
isRightsPath,
|
|
308
|
-
roleFromRequest,
|
|
309
|
-
wrapListener,
|
|
310
|
-
};
|
package/src-admin/README.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# NexoWatt EOS Admin Frontend Source
|
|
2
|
-
|
|
3
|
-
Dieser Bereich enthält den Frontend-Quellcode der Administrationsoberfläche. Das sichtbare EOS-Branding wird über die fertigen Assets in `adminWww/` und die NexoWatt-spezifischen Branding-Dateien ergänzt.
|
|
4
|
-
|
|
5
|
-
Die folgenden Komponentenhinweise stammen aus der technischen Frontend-Basis und bleiben für Entwicklung und Wartung erhalten.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Components
|
|
10
|
-
|
|
11
|
-
## DrawerItem
|
|
12
|
-
|
|
13
|
-

|
|
14
|
-

|
|
15
|
-

|
|
16
|
-
|
|
17
|
-
### Import
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
import DrawerItem from './components/DrawerItem';
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Props
|
|
24
|
-
|
|
25
|
-
| Name | Default | Description |
|
|
26
|
-
| ------------ | ------- | -------------------------------------- |
|
|
27
|
-
| icon | | The icon on the left side |
|
|
28
|
-
| onClick | | Function that is called when clicking |
|
|
29
|
-
| selected | | Indicates whether the item is selected |
|
|
30
|
-
| text | | Text to display |
|
|
31
|
-
| badgeContent | 0 | Content of the badge |
|
|
32
|
-
| badgeColor | primary | Color of the badge |
|
|
33
|
-
|
|
34
|
-
### Example
|
|
35
|
-
|
|
36
|
-
```js
|
|
37
|
-
<DrawerItem
|
|
38
|
-
text="Logs"
|
|
39
|
-
selected={false}
|
|
40
|
-
badgeContent={11}
|
|
41
|
-
badgeColor={'error'}
|
|
42
|
-
/>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## IntroCard
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-

|
|
49
|
-

|
|
50
|
-
|
|
51
|
-
### Import
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
import IntroCard from './components/IntroCard';
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Props
|
|
58
|
-
|
|
59
|
-
| Name | Default | Description |
|
|
60
|
-
|------------------| ------- |-------------------------------------------------------------------------------------|
|
|
61
|
-
| title | | The title of the card |
|
|
62
|
-
| children | | The child elements |
|
|
63
|
-
| image | | The image to display on the left |
|
|
64
|
-
| color | #e2e2e2 | The color to display as the background of the image |
|
|
65
|
-
| action | | Link and text {link: 'https://example.com', text: 'example.com'} |
|
|
66
|
-
| reveal | | Element or text that should be displayed in the reveal element using an info button |
|
|
67
|
-
| enabled | | State of the card |
|
|
68
|
-
| edit | | Editing mode active |
|
|
69
|
-
| toggleActivation | | Function for switching the status |
|