@lenne.tech/cli 1.8.0 → 1.9.0
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/build/commands/blocks/add.js +4 -3
- package/build/commands/blocks/blocks.js +1 -1
- package/build/commands/claude/plugins.js +17 -12
- package/build/commands/cli/create.js +1 -1
- package/build/commands/cli/rename.js +1 -1
- package/build/commands/completion.js +1 -1
- package/build/commands/components/add.js +4 -3
- package/build/commands/components/components.js +1 -1
- package/build/commands/config/help.js +1 -1
- package/build/commands/config/validate.js +10 -1
- package/build/commands/directus/docker-setup.js +1 -1
- package/build/commands/directus/remove.js +2 -3
- package/build/commands/directus/typegen.js +1 -1
- package/build/commands/doctor.js +4 -4
- package/build/commands/frontend/angular.js +1 -1
- package/build/commands/frontend/nuxt.js +2 -2
- package/build/commands/fullstack/init.js +14 -10
- package/build/commands/git/clean.js +6 -6
- package/build/commands/git/force-pull.js +6 -3
- package/build/commands/git/get.js +2 -3
- package/build/commands/git/install-scripts.js +2 -2
- package/build/commands/git/rebase.js +5 -2
- package/build/commands/git/rename.js +3 -4
- package/build/commands/git/reset.js +14 -11
- package/build/commands/git/squash.js +5 -2
- package/build/commands/git/undo.js +6 -3
- package/build/commands/git/update.js +3 -3
- package/build/commands/history.js +1 -1
- package/build/commands/mongodb/s3-restore.js +10 -7
- package/build/commands/qdrant/delete.js +2 -2
- package/build/commands/qdrant/stats.js +2 -11
- package/build/commands/server/add-property.js +46 -31
- package/build/commands/server/create.js +5 -3
- package/build/commands/server/module.js +30 -13
- package/build/commands/server/object.js +1 -1
- package/build/commands/server/permissions.js +318 -0
- package/build/commands/server/set-secrets.js +2 -2
- package/build/commands/status.js +13 -19
- package/build/commands/templates/list.js +4 -4
- package/build/commands/templates/llm.js +8 -14
- package/build/commands/tools/install-scripts.js +2 -2
- package/build/commands/tools/regex.js +1 -1
- package/build/extensions/api-mode.js +3 -2
- package/build/extensions/config.js +5 -5
- package/build/extensions/frontend-helper.js +1 -1
- package/build/extensions/git.js +11 -11
- package/build/extensions/history.js +1 -1
- package/build/extensions/logger.js +3 -6
- package/build/extensions/package-manager.js +6 -6
- package/build/extensions/parse-properties.js +4 -4
- package/build/extensions/server.js +35 -23
- package/build/extensions/template.js +5 -5
- package/build/extensions/tools.js +5 -5
- package/build/extensions/typescript.js +2 -2
- package/build/lib/claude-cli.js +2 -6
- package/build/lib/fallback-scanner.js +852 -0
- package/build/lib/marketplace.js +7 -7
- package/build/lib/nuxt-base-components.js +3 -3
- package/build/lib/plugin-utils.js +9 -13
- package/build/lib/shell-config.js +5 -8
- package/build/lib/validation.js +5 -5
- package/build/templates/permissions/report.html.ejs +402 -0
- package/docs/commands.md +24 -0
- package/docs/lt.config.md +31 -0
- package/package.json +2 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Permissions Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #ffffff; --fg: #1a1a2e; --bg-card: #f8f9fa; --border: #dee2e6;
|
|
10
|
+
--primary: #0d6efd; --success: #198754; --warning: #ffc107; --danger: #dc3545;
|
|
11
|
+
--role-everyone: #198754; --role-noone: #dc3545; --role-admin: #0d6efd;
|
|
12
|
+
--role-user: #e6a817; --role-self: #6c757d; --role-custom: #fd7e14;
|
|
13
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.12);
|
|
14
|
+
}
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #1a1a2e; --fg: #e0e0e0; --bg-card: #16213e; --border: #3a3a5c;
|
|
18
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
22
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; }
|
|
23
|
+
.layout { display: flex; min-height: 100vh; }
|
|
24
|
+
.sidebar { width: 260px; position: sticky; top: 0; height: 100vh; overflow-y: auto; background: var(--bg-card); border-right: 1px solid var(--border); padding: 1rem; flex-shrink: 0; }
|
|
25
|
+
.sidebar h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--primary); margin-bottom: 0.5rem; }
|
|
26
|
+
.sidebar a { display: block; padding: 0.25rem 0.5rem; color: var(--fg); text-decoration: none; font-size: 0.85rem; border-radius: 4px; }
|
|
27
|
+
.sidebar a:hover { background: var(--border); }
|
|
28
|
+
.sidebar a.indent { padding-left: 1.5rem; font-size: 0.8rem; opacity: 0.8; }
|
|
29
|
+
.main { flex: 1; padding: 2rem; max-width: 1200px; }
|
|
30
|
+
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
31
|
+
.stat { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; text-align: center; box-shadow: var(--shadow); }
|
|
32
|
+
.stat .num { font-size: 2rem; font-weight: 700; color: var(--primary); }
|
|
33
|
+
.stat .label { font-size: 0.8rem; opacity: 0.7; }
|
|
34
|
+
.stat.warn .num { color: var(--warning); }
|
|
35
|
+
.stat.danger .num { color: var(--danger); }
|
|
36
|
+
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; align-items: center; }
|
|
37
|
+
.controls input, .controls select { padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); font-size: 0.9rem; }
|
|
38
|
+
.controls input { flex: 1; min-width: 200px; }
|
|
39
|
+
.controls label { font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem; }
|
|
40
|
+
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; color: #fff; }
|
|
41
|
+
.badge-everyone { background: var(--role-everyone); }
|
|
42
|
+
.badge-noone { background: var(--role-noone); }
|
|
43
|
+
.badge-admin { background: var(--role-admin); }
|
|
44
|
+
.badge-user { background: var(--role-user); color: #000; }
|
|
45
|
+
.badge-self, .badge-creator { background: var(--role-self); }
|
|
46
|
+
.badge-custom { background: var(--role-custom); }
|
|
47
|
+
.badge-warn { background: var(--warning); color: #000; }
|
|
48
|
+
section { margin-bottom: 2rem; }
|
|
49
|
+
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
|
|
50
|
+
h2 { font-size: 1.4rem; margin: 1.5rem 0 0.75rem; padding-bottom: 0.3rem; border-bottom: 2px solid var(--primary); }
|
|
51
|
+
h3 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }
|
|
52
|
+
.meta { font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.25rem; }
|
|
53
|
+
table { width: 100%; border-collapse: collapse; margin: 0.5rem 0 1rem; font-size: 0.85rem; }
|
|
54
|
+
th, td { padding: 0.4rem 0.6rem; text-align: left; border: 1px solid var(--border); }
|
|
55
|
+
th { background: var(--bg-card); cursor: pointer; user-select: none; white-space: nowrap; }
|
|
56
|
+
th:hover { background: var(--border); }
|
|
57
|
+
tr:nth-child(even) { background: var(--bg-card); }
|
|
58
|
+
.collapsible { cursor: pointer; }
|
|
59
|
+
.collapsible::before { content: '\25B6'; display: inline-block; margin-right: 0.5rem; transition: transform 0.2s; font-size: 0.8rem; }
|
|
60
|
+
.collapsible.open::before { transform: rotate(90deg); }
|
|
61
|
+
.collapse-content { display: none; }
|
|
62
|
+
.collapse-content.open { display: block; }
|
|
63
|
+
.warning-row { background: rgba(255, 193, 7, 0.1) !important; }
|
|
64
|
+
.btn { padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); color: var(--fg); cursor: pointer; font-size: 0.8rem; }
|
|
65
|
+
.btn:hover { background: var(--border); }
|
|
66
|
+
.module-header { position: sticky; top: 0; background: var(--bg); z-index: 10; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
|
|
67
|
+
@media (max-width: 768px) {
|
|
68
|
+
.sidebar { display: none; }
|
|
69
|
+
.main { padding: 1rem; }
|
|
70
|
+
.dashboard { grid-template-columns: repeat(2, 1fr); }
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div class="layout">
|
|
76
|
+
<nav class="sidebar">
|
|
77
|
+
<h3>Permissions Report</h3>
|
|
78
|
+
<a href="#dashboard">Dashboard</a>
|
|
79
|
+
<a href="#role-index">Role Index</a>
|
|
80
|
+
<a href="#warnings">Warnings (<%= props.warnings.length %>)</a>
|
|
81
|
+
<hr style="margin:0.5rem 0;border-color:var(--border)">
|
|
82
|
+
<% for (const mod of props.modules) { %>
|
|
83
|
+
<a href="#mod-<%= mod.name %>"><%= mod.name %></a>
|
|
84
|
+
<% for (const model of mod.models) { %><a href="#model-<%= mod.name %>-<%= model.className %>" class="indent">Model: <%= model.className %></a><% } %>
|
|
85
|
+
<% for (const ctrl of mod.controllers) { %><a href="#ctrl-<%= mod.name %>-<%= ctrl.className %>" class="indent">Ctrl: <%= ctrl.className %></a><% } %>
|
|
86
|
+
<% for (const res of mod.resolvers) { %><a href="#res-<%= mod.name %>-<%= res.className %>" class="indent">Resolver: <%= res.className %></a><% } %>
|
|
87
|
+
<% } %>
|
|
88
|
+
<% if (props.objects.length > 0) { %>
|
|
89
|
+
<hr style="margin:0.5rem 0;border-color:var(--border)">
|
|
90
|
+
<a href="#subobjects">SubObjects</a>
|
|
91
|
+
<% } %>
|
|
92
|
+
<hr style="margin:0.5rem 0;border-color:var(--border)">
|
|
93
|
+
<button class="btn" onclick="exportAs('md')">Export MD</button>
|
|
94
|
+
<button class="btn" onclick="exportAs('json')">Export JSON</button>
|
|
95
|
+
</nav>
|
|
96
|
+
|
|
97
|
+
<div class="main">
|
|
98
|
+
<h1>Permissions Report</h1>
|
|
99
|
+
<p class="meta">Generated: <%= props.generated %></p>
|
|
100
|
+
<p class="meta">Project: <%= props.projectPath %></p>
|
|
101
|
+
|
|
102
|
+
<section id="dashboard">
|
|
103
|
+
<div class="dashboard">
|
|
104
|
+
<div class="stat"><div class="num"><%= props.stats.totalModules %></div><div class="label">Modules</div></div>
|
|
105
|
+
<div class="stat"><div class="num"><%= props.stats.totalModels %></div><div class="label">Models</div></div>
|
|
106
|
+
<div class="stat"><div class="num"><%= props.stats.totalEndpoints %></div><div class="label">Endpoints</div></div>
|
|
107
|
+
<div class="stat"><div class="num"><%= props.stats.totalSubObjects %></div><div class="label">SubObjects</div></div>
|
|
108
|
+
<div class="stat <%= props.stats.totalWarnings > 0 ? 'danger' : '' %>"><div class="num"><%= props.stats.totalWarnings %></div><div class="label">Warnings</div></div>
|
|
109
|
+
<% var ecClass = props.stats.endpointCoverage >= 90 ? '' : props.stats.endpointCoverage >= 70 ? 'warn' : 'danger'; %>
|
|
110
|
+
<div class="stat <%= ecClass %>"><div class="num"><%= props.stats.endpointCoverage %>%</div><div class="label">Endpoint Coverage</div></div>
|
|
111
|
+
<% var scClass = props.stats.securityCoverage >= 90 ? '' : props.stats.securityCoverage >= 70 ? 'warn' : 'danger'; %>
|
|
112
|
+
<div class="stat <%= scClass %>"><div class="num"><%= props.stats.securityCoverage %>%</div><div class="label">Security Coverage</div></div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<section>
|
|
117
|
+
<div class="controls">
|
|
118
|
+
<input type="text" id="search" placeholder="Search modules, fields, roles..." oninput="filterAll()">
|
|
119
|
+
<select id="roleFilter" onchange="filterAll()">
|
|
120
|
+
<option value="">All Roles</option>
|
|
121
|
+
</select>
|
|
122
|
+
<label><input type="checkbox" id="warnOnly" onchange="filterAll()"> Warnings only</label>
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<section id="role-index">
|
|
127
|
+
<h2 class="collapsible open" onclick="toggle(this)">Role Index</h2>
|
|
128
|
+
<div class="collapse-content open">
|
|
129
|
+
<% if (props.roleEnums.length > 0) { %>
|
|
130
|
+
<table>
|
|
131
|
+
<thead><tr><th>Enum</th><th>Value</th><th>Type</th></tr></thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
<% for (const e of props.roleEnums) { for (const v of e.values) { %>
|
|
134
|
+
<tr>
|
|
135
|
+
<td><%= e.name %>.<%= v.key %></td>
|
|
136
|
+
<td><%= v.key.startsWith('S_') ? '(system)' : v.value %></td>
|
|
137
|
+
<td><span class="badge <%= getBadgeClass(v.key) %>"><%= v.key.startsWith('S_') ? 'System' : 'Real' %></span></td>
|
|
138
|
+
</tr>
|
|
139
|
+
<% } } %>
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
<% } else { %>
|
|
143
|
+
<p><em>No role enums found.</em></p>
|
|
144
|
+
<% } %>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
|
|
148
|
+
<section id="warnings">
|
|
149
|
+
<h2 class="collapsible open" onclick="toggle(this)">Warnings (<%= props.warnings.length %>)</h2>
|
|
150
|
+
<div class="collapse-content open">
|
|
151
|
+
<% if (props.warnings.length > 0) { %>
|
|
152
|
+
<table>
|
|
153
|
+
<thead><tr><th>#</th><th>Module</th><th>File</th><th>Type</th><th>Details</th></tr></thead>
|
|
154
|
+
<tbody>
|
|
155
|
+
<% props.warnings.forEach((w, i) => { %>
|
|
156
|
+
<tr class="warning-row">
|
|
157
|
+
<td><%= i + 1 %></td>
|
|
158
|
+
<td><%= w.module %></td>
|
|
159
|
+
<td><%= w.file.split('/').pop() %></td>
|
|
160
|
+
<td><span class="badge badge-warn"><%= w.type %></span></td>
|
|
161
|
+
<td><%= w.details %></td>
|
|
162
|
+
</tr>
|
|
163
|
+
<% }) %>
|
|
164
|
+
</tbody>
|
|
165
|
+
</table>
|
|
166
|
+
<% } else { %>
|
|
167
|
+
<p><em>No warnings found.</em></p>
|
|
168
|
+
<% } %>
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
|
|
172
|
+
<% for (const mod of props.modules) { %>
|
|
173
|
+
<section class="module-section" id="mod-<%= mod.name %>" data-module="<%= mod.name %>" data-has-warnings="<%= props.warnings.some(w => w.module === mod.name) %>">
|
|
174
|
+
<div class="module-header">
|
|
175
|
+
<h2 class="collapsible open" onclick="toggle(this)">Module: <%= mod.name %></h2>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="collapse-content open">
|
|
178
|
+
|
|
179
|
+
<% for (const model of mod.models) { %>
|
|
180
|
+
<div id="model-<%= mod.name %>-<%= model.className %>">
|
|
181
|
+
<h3>Model: <%= model.className %></h3>
|
|
182
|
+
<p class="meta">File: <%= model.filePath %></p>
|
|
183
|
+
<% if (model.extendsClass) { %><p class="meta">Extends: <%= model.extendsClass %></p><% } %>
|
|
184
|
+
<p class="meta">Class Restriction: <% if (model.classRestriction.length > 0) { model.classRestriction.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
|
|
185
|
+
<p class="meta">securityCheck: <% if (model.securityCheck) { %><%= model.securityCheck.summary %><% } else { %><em>Not present</em><% } %></p>
|
|
186
|
+
<% if (model.fields.length > 0) { %>
|
|
187
|
+
<table>
|
|
188
|
+
<thead><tr><th>Field</th><th>Roles</th><th>Source</th></tr></thead>
|
|
189
|
+
<tbody>
|
|
190
|
+
<% for (const f of model.fields) { %>
|
|
191
|
+
<tr>
|
|
192
|
+
<td><%= f.name %></td>
|
|
193
|
+
<td><%= f.roles %></td>
|
|
194
|
+
<td><%= f.inherited ? 'inherited' : 'local' %></td>
|
|
195
|
+
</tr>
|
|
196
|
+
<% } %>
|
|
197
|
+
</tbody>
|
|
198
|
+
</table>
|
|
199
|
+
<% } %>
|
|
200
|
+
</div>
|
|
201
|
+
<% } %>
|
|
202
|
+
|
|
203
|
+
<% for (const input of mod.inputs) { %>
|
|
204
|
+
<div>
|
|
205
|
+
<h3>Input: <%= input.className %></h3>
|
|
206
|
+
<p class="meta">File: <%= input.filePath %></p>
|
|
207
|
+
<% if (input.extendsClass) { %><p class="meta">Extends: <%= input.extendsClass %></p><% } %>
|
|
208
|
+
<% if (input.fields.length > 0) { %>
|
|
209
|
+
<table>
|
|
210
|
+
<thead><tr><th>Field</th><th>Roles</th></tr></thead>
|
|
211
|
+
<tbody>
|
|
212
|
+
<% for (const f of input.fields) { %>
|
|
213
|
+
<tr><td><%= f.name %></td><td><%= f.roles %></td></tr>
|
|
214
|
+
<% } %>
|
|
215
|
+
</tbody>
|
|
216
|
+
</table>
|
|
217
|
+
<% } %>
|
|
218
|
+
</div>
|
|
219
|
+
<% } %>
|
|
220
|
+
|
|
221
|
+
<% for (const ctrl of mod.controllers) { %>
|
|
222
|
+
<div id="ctrl-<%= mod.name %>-<%= ctrl.className %>">
|
|
223
|
+
<h3>Controller: <%= ctrl.className %></h3>
|
|
224
|
+
<p class="meta">File: <%= ctrl.filePath %></p>
|
|
225
|
+
<p class="meta">Class Roles: <% if (ctrl.classRoles.length > 0) { ctrl.classRoles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
|
|
226
|
+
<% if (ctrl.methods.length > 0) { %>
|
|
227
|
+
<table>
|
|
228
|
+
<thead><tr><th>Method</th><th>HTTP</th><th>Route</th><th>Roles</th><th>Effective</th></tr></thead>
|
|
229
|
+
<tbody>
|
|
230
|
+
<% for (const m of ctrl.methods) { const eff = m.roles.length > 0 ? m.roles : ctrl.classRoles; %>
|
|
231
|
+
<tr>
|
|
232
|
+
<td><%= m.name %></td>
|
|
233
|
+
<td><%= m.httpMethod %></td>
|
|
234
|
+
<td><%= m.route || '/' %></td>
|
|
235
|
+
<td><% if (m.roles.length > 0) { m.roles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></td>
|
|
236
|
+
<td><% eff.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) %><%= m.roles.length === 0 && ctrl.classRoles.length > 0 ? '(class)' : '' %></td>
|
|
237
|
+
</tr>
|
|
238
|
+
<% } %>
|
|
239
|
+
</tbody>
|
|
240
|
+
</table>
|
|
241
|
+
<% } %>
|
|
242
|
+
</div>
|
|
243
|
+
<% } %>
|
|
244
|
+
|
|
245
|
+
<% for (const res of mod.resolvers) { %>
|
|
246
|
+
<div id="res-<%= mod.name %>-<%= res.className %>">
|
|
247
|
+
<h3>Resolver: <%= res.className %></h3>
|
|
248
|
+
<p class="meta">File: <%= res.filePath %></p>
|
|
249
|
+
<p class="meta">Class Roles: <% if (res.classRoles.length > 0) { res.classRoles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
|
|
250
|
+
<% if (res.methods.length > 0) { %>
|
|
251
|
+
<table>
|
|
252
|
+
<thead><tr><th>Method</th><th>Type</th><th>Roles</th><th>Effective</th></tr></thead>
|
|
253
|
+
<tbody>
|
|
254
|
+
<% for (const m of res.methods) { const eff = m.roles.length > 0 ? m.roles : res.classRoles; %>
|
|
255
|
+
<tr>
|
|
256
|
+
<td><%= m.name %></td>
|
|
257
|
+
<td><%= m.httpMethod %></td>
|
|
258
|
+
<td><% if (m.roles.length > 0) { m.roles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></td>
|
|
259
|
+
<td><% eff.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) %><%= m.roles.length === 0 && res.classRoles.length > 0 ? '(class)' : '' %></td>
|
|
260
|
+
</tr>
|
|
261
|
+
<% } %>
|
|
262
|
+
</tbody>
|
|
263
|
+
</table>
|
|
264
|
+
<% } %>
|
|
265
|
+
</div>
|
|
266
|
+
<% } %>
|
|
267
|
+
|
|
268
|
+
</div>
|
|
269
|
+
</section>
|
|
270
|
+
<% } %>
|
|
271
|
+
|
|
272
|
+
<% if (props.objects.length > 0) { %>
|
|
273
|
+
<section id="subobjects">
|
|
274
|
+
<h2>SubObjects</h2>
|
|
275
|
+
<% for (const obj of props.objects) { %>
|
|
276
|
+
<div>
|
|
277
|
+
<h3><%= obj.className %></h3>
|
|
278
|
+
<p class="meta">File: <%= obj.filePath %></p>
|
|
279
|
+
<% if (obj.extendsClass) { %><p class="meta">Extends: <%= obj.extendsClass %></p><% } %>
|
|
280
|
+
<% if (obj.fields.length > 0) { %>
|
|
281
|
+
<table>
|
|
282
|
+
<thead><tr><th>Field</th><th>Roles</th><th>Source</th></tr></thead>
|
|
283
|
+
<tbody>
|
|
284
|
+
<% for (const f of obj.fields) { %>
|
|
285
|
+
<tr><td><%= f.name %></td><td><%= f.roles %></td><td><%= f.inherited ? 'inherited' : 'local' %></td></tr>
|
|
286
|
+
<% } %>
|
|
287
|
+
</tbody>
|
|
288
|
+
</table>
|
|
289
|
+
<% } %>
|
|
290
|
+
</div>
|
|
291
|
+
<% } %>
|
|
292
|
+
</section>
|
|
293
|
+
<% } %>
|
|
294
|
+
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<script>
|
|
299
|
+
var DATA = <%- JSON.stringify({ modules: props.modules, objects: props.objects, roleEnums: props.roleEnums, stats: props.stats, warnings: props.warnings }) %>;
|
|
300
|
+
|
|
301
|
+
// Populate role filter
|
|
302
|
+
(function() {
|
|
303
|
+
var sel = document.getElementById('roleFilter');
|
|
304
|
+
var roles = {};
|
|
305
|
+
DATA.roleEnums.forEach(function(e) { e.values.forEach(function(v) { roles[v.key] = true; }); });
|
|
306
|
+
DATA.modules.forEach(function(mod) {
|
|
307
|
+
mod.models.forEach(function(model) {
|
|
308
|
+
model.classRestriction.forEach(function(r) { roles[r] = true; });
|
|
309
|
+
model.fields.forEach(function(f) {
|
|
310
|
+
var m = f.roles.match(/`([^`]+)`/g);
|
|
311
|
+
if (m) m.forEach(function(r) { roles[r.replace(/`/g, '')] = true; });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
mod.controllers.forEach(function(ep) {
|
|
315
|
+
ep.classRoles.forEach(function(r) { roles[r] = true; });
|
|
316
|
+
ep.methods.forEach(function(m) { m.roles.forEach(function(r) { roles[r] = true; }); });
|
|
317
|
+
});
|
|
318
|
+
mod.resolvers.forEach(function(ep) {
|
|
319
|
+
ep.classRoles.forEach(function(r) { roles[r] = true; });
|
|
320
|
+
ep.methods.forEach(function(m) { m.roles.forEach(function(r) { roles[r] = true; }); });
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
Object.keys(roles).sort().forEach(function(r) {
|
|
324
|
+
var o = document.createElement('option');
|
|
325
|
+
o.value = r; o.textContent = r;
|
|
326
|
+
sel.appendChild(o);
|
|
327
|
+
});
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
function toggle(el) {
|
|
331
|
+
el.classList.toggle('open');
|
|
332
|
+
var content = el.nextElementSibling;
|
|
333
|
+
if (content) content.classList.toggle('open');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function filterAll() {
|
|
337
|
+
var q = document.getElementById('search').value.toLowerCase();
|
|
338
|
+
var role = document.getElementById('roleFilter').value;
|
|
339
|
+
var warnOnly = document.getElementById('warnOnly').checked;
|
|
340
|
+
|
|
341
|
+
document.querySelectorAll('.module-section').forEach(function(section) {
|
|
342
|
+
var hasWarnings = section.dataset.hasWarnings === 'true';
|
|
343
|
+
var text = section.textContent.toLowerCase();
|
|
344
|
+
var show = true;
|
|
345
|
+
if (q && !text.includes(q)) show = false;
|
|
346
|
+
if (warnOnly && !hasWarnings) show = false;
|
|
347
|
+
if (role && !text.includes(role.toLowerCase())) show = false;
|
|
348
|
+
section.style.display = show ? '' : 'none';
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
var warningsSection = document.getElementById('warnings');
|
|
352
|
+
if (warningsSection) {
|
|
353
|
+
warningsSection.querySelectorAll('tbody tr').forEach(function(row) {
|
|
354
|
+
var text = row.textContent.toLowerCase();
|
|
355
|
+
var show = true;
|
|
356
|
+
if (q && !text.includes(q)) show = false;
|
|
357
|
+
row.style.display = show ? '' : 'none';
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
document.querySelectorAll('th').forEach(function(th) {
|
|
363
|
+
th.addEventListener('click', function() {
|
|
364
|
+
var table = this.closest('table');
|
|
365
|
+
var tbody = table.querySelector('tbody');
|
|
366
|
+
if (!tbody) return;
|
|
367
|
+
var idx = Array.from(this.parentNode.children).indexOf(this);
|
|
368
|
+
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
369
|
+
var asc = this.dataset.sort !== 'asc';
|
|
370
|
+
rows.sort(function(a, b) {
|
|
371
|
+
var at = (a.children[idx] || {}).textContent || '';
|
|
372
|
+
var bt = (b.children[idx] || {}).textContent || '';
|
|
373
|
+
return asc ? at.localeCompare(bt) : bt.localeCompare(at);
|
|
374
|
+
});
|
|
375
|
+
this.dataset.sort = asc ? 'asc' : 'desc';
|
|
376
|
+
rows.forEach(function(r) { tbody.appendChild(r); });
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
function exportAs(fmt) {
|
|
381
|
+
var blob = new Blob([JSON.stringify(DATA, null, 2)], { type: fmt === 'json' ? 'application/json' : 'text/plain' });
|
|
382
|
+
var a = document.createElement('a');
|
|
383
|
+
a.href = URL.createObjectURL(blob);
|
|
384
|
+
a.download = 'permissions.' + fmt;
|
|
385
|
+
a.click();
|
|
386
|
+
}
|
|
387
|
+
</script>
|
|
388
|
+
</body>
|
|
389
|
+
</html>
|
|
390
|
+
<%
|
|
391
|
+
function getBadgeClass(role) {
|
|
392
|
+
if (!role) return 'badge-custom';
|
|
393
|
+
var r = role.toUpperCase();
|
|
394
|
+
if (r === 'S_EVERYONE') return 'badge-everyone';
|
|
395
|
+
if (r === 'S_NO_ONE') return 'badge-noone';
|
|
396
|
+
if (r === 'ADMIN') return 'badge-admin';
|
|
397
|
+
if (r === 'S_USER') return 'badge-user';
|
|
398
|
+
if (r === 'S_SELF') return 'badge-self';
|
|
399
|
+
if (r === 'S_CREATOR') return 'badge-creator';
|
|
400
|
+
return 'badge-custom';
|
|
401
|
+
}
|
|
402
|
+
%>
|
package/docs/commands.md
CHANGED
|
@@ -123,6 +123,30 @@ lt server object [options]
|
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
|
+
### `lt server permissions`
|
|
127
|
+
|
|
128
|
+
Scans all server modules and generates a permissions report showing roles, restrictions, and security gaps.
|
|
129
|
+
|
|
130
|
+
**Usage:**
|
|
131
|
+
```bash
|
|
132
|
+
lt server permissions [options]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Options:**
|
|
136
|
+
| Option | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `--path <dir>` | Path to NestJS project (default: auto-detect) |
|
|
139
|
+
| `--output <file>` | Output file (default: `permissions.<format>`) |
|
|
140
|
+
| `--format <md\|json\|html>` | Output format (default: `html` for TTY, `json` for CI) |
|
|
141
|
+
| `--open` / `--no-open` | Open report in browser (default: `true` for TTY) |
|
|
142
|
+
| `--console` | Print summary to console |
|
|
143
|
+
| `--fail-on-warnings` | Exit code 1 on warnings (for CI/CD) |
|
|
144
|
+
| `--noConfirm` | Skip confirmation prompts |
|
|
145
|
+
|
|
146
|
+
**Configuration:** `commands.server.permissions.*`, `defaults.noConfirm`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
126
150
|
### `lt server addProp`
|
|
127
151
|
|
|
128
152
|
Adds a property to an existing module or object.
|
package/docs/lt.config.md
CHANGED
|
@@ -348,6 +348,37 @@ Creates a new server object (embedded document).
|
|
|
348
348
|
|
|
349
349
|
---
|
|
350
350
|
|
|
351
|
+
#### `lt server permissions`
|
|
352
|
+
|
|
353
|
+
Scans server modules and generates a permissions report.
|
|
354
|
+
|
|
355
|
+
| Field | Type | Default | Description |
|
|
356
|
+
|-------|------|---------|-------------|
|
|
357
|
+
| `commands.server.permissions.format` | `'md'` \| `'json'` \| `'html'` | `'html'` (TTY) / `'json'` (CI) | Output format |
|
|
358
|
+
| `commands.server.permissions.output` | `string` | `permissions.<format>` | Output file path |
|
|
359
|
+
| `commands.server.permissions.path` | `string` | auto-detect | Path to NestJS project |
|
|
360
|
+
| `commands.server.permissions.open` | `boolean` | `true` (TTY) / `false` (CI) | Open report in browser |
|
|
361
|
+
| `commands.server.permissions.console` | `boolean` | `false` | Print summary to console |
|
|
362
|
+
| `commands.server.permissions.failOnWarnings` | `boolean` | `false` | Exit code 1 on warnings |
|
|
363
|
+
| `commands.server.permissions.noConfirm` | `boolean` | `false` | Skip confirmation prompts |
|
|
364
|
+
|
|
365
|
+
**Example:**
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"commands": {
|
|
369
|
+
"server": {
|
|
370
|
+
"permissions": {
|
|
371
|
+
"format": "html",
|
|
372
|
+
"open": true,
|
|
373
|
+
"failOnWarnings": true
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
351
382
|
#### `lt server addProp`
|
|
352
383
|
|
|
353
384
|
Adds a property to an existing module or object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"lt": "bin/lt"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
+
"check": "npm install && npm run format && npm run build && npm run start",
|
|
21
22
|
"postinstall": "node bin/postinstall.js 2>/dev/null || true",
|
|
22
23
|
"build": "npm run lint && npm run test && npm run clean-build && npm run compile && npm run copy-templates",
|
|
23
24
|
"clean-build": "npx rimraf ./build",
|