@techie_doubts/tui.notes.2026 1.0.14 → 1.0.16-exp.1
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/README.md +50 -0
- package/dist/assets/{arc-Dw2FFQpg.js → arc-PCH7RH34.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CufEy62L.js → architectureDiagram-VXUJARFQ-CJCgNTQY.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-BnqXETC-.js → blockDiagram-VD42YOAC-Dr6BuZdb.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-CRRqnF8Q.js → c4Diagram-YG6GDRKO-Cujs6tue.js} +1 -1
- package/dist/assets/channel-CcerkviR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BKdUMn0k.js → chunk-4BX2VUAB-BcD-NEr2.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-B8U-4L0g.js → chunk-55IACEB6-CuADq_qH.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-DiSlffG3.js → chunk-B4BG7PRW-CQeb3pmx.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-DoFW5sIs.js → chunk-DI55MBZ5-CLozoy-P.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-G-X5BCqS.js → chunk-FMBD7UC4-DTHBR6EW.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-D_F0QBne.js → chunk-QN33PNHL-ChElDE3F.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-yGAa1Z7-.js → chunk-QZHKN3VN-BOuzHqeF.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-gDf9m9hb.js → chunk-TZMSLE5B-BWbpFvic.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-DuvJikuL.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DuvJikuL.js +1 -0
- package/dist/assets/{clone-CtQPqq7L.js → clone-CZ5tMkEs.js} +1 -1
- package/dist/assets/{cose-bilkent-S5V4N54A-DWUmsMRp.js → cose-bilkent-S5V4N54A-BrtvxZJ8.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-B2BazWXF.js → dagre-6UL2VRFP-DLURHD-1.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-BpBMH0Ts.js → diagram-PSM6KHXK-D5gDu3Jy.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DT4ajz1c.js → diagram-QEK2KX5R-DHhyRXC6.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-DFOpAZP9.js → diagram-S2PKOQOG-DB2mF9n1.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-C5AMhI0K.js → erDiagram-Q2GNP2WA-CuYjTYDE.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-CeGyvCMA.js → flowDiagram-NV44I4VS-BTmXRhk3.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-uVJ-OV6s.js → ganttDiagram-JELNMOA3-Dj8216Bt.js} +1 -1
- package/dist/assets/{gitGraphDiagram-NY62KEGX-BrhQjAjl.js → gitGraphDiagram-NY62KEGX-AfR_zz8Y.js} +1 -1
- package/dist/assets/{index-CNp6F8X7.css → index-8Yfq90k_.css} +1 -1
- package/dist/assets/{index-c2yhXVIu.js → index-CORObQTV.js} +594 -517
- package/dist/assets/{infoDiagram-WHAUD3N6-CRdDYgIS.js → infoDiagram-WHAUD3N6-B6ZVQAG8.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-Yqs1G7Ur.js → journeyDiagram-XKPGCS4Q-BoG7zgLk.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-GIkJLZdc.js → kanban-definition-3W4ZIXB7-CGkltrpk.js} +1 -1
- package/dist/assets/{linear-Bx_xGOD8.js → linear-ZFsx-qcY.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-Bh9_I7_C.js → mindmap-definition-VGOIOE7T-DS3-vFjB.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-_IIkl2Vj.js → pieDiagram-ADFJNKIX-DS4SKK5L.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-BeN-mDm1.js → quadrantDiagram-AYHSOK5B-BqSmZSFq.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-DTuux7i8.js → requirementDiagram-UZGBJVZJ-MQtqXGHx.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-BnEwJfWi.js → sankeyDiagram-TZEHDZUN-DPxJ0i0v.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-CKDNxA0n.js → sequenceDiagram-WL72ISMW-0yXSvFmS.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-B5NsJSxj.js → stateDiagram-FKZM4ZOC-e_TqYmQe.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-B85_wtn8.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-90xY8Nw6.js → timeline-definition-IT6M3QCI-mfjyRSAU.js} +1 -1
- package/dist/assets/{treemap-KMMF4GRG-CT9Jf2NQ.js → treemap-KMMF4GRG-CxwWdtjF.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-CLAphPTr.js → xychartDiagram-PRI3JC2R-DzSzO4xZ.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -5
- package/server/acl-service.js +1249 -0
- package/server/acl-service.test.js +439 -0
- package/server/acl-store.js +421 -0
- package/server/auth.js +119 -0
- package/server/index.js +719 -23
- package/server/store.js +12 -0
- package/dist/assets/channel-DrL0fpY9.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-BHSxN04i.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-BHSxN04i.js +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DvbILQz6.js +0 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function nowMs() {
|
|
6
|
+
return Date.now();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function createId() {
|
|
10
|
+
if (typeof crypto.randomUUID === "function") {
|
|
11
|
+
return crypto.randomUUID();
|
|
12
|
+
}
|
|
13
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeResource(resource) {
|
|
17
|
+
return {
|
|
18
|
+
type: String(resource?.type || "").trim(),
|
|
19
|
+
externalId: String(resource?.externalId || "").trim(),
|
|
20
|
+
parentType: resource?.parentType ? String(resource.parentType).trim() : null,
|
|
21
|
+
parentExternalId: resource?.parentExternalId ? String(resource.parentExternalId).trim() : null,
|
|
22
|
+
ownerSubject: resource?.ownerSubject ? String(resource.ownerSubject).trim() : null,
|
|
23
|
+
createdAt: Number(resource?.createdAt) || nowMs(),
|
|
24
|
+
updatedAt: Number(resource?.updatedAt) || nowMs(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeBinding(binding) {
|
|
29
|
+
return {
|
|
30
|
+
id: String(binding?.id || createId()).trim(),
|
|
31
|
+
resourceType: String(binding?.resourceType || "").trim(),
|
|
32
|
+
resourceExternalId: String(binding?.resourceExternalId || "").trim(),
|
|
33
|
+
subjectType: String(binding?.subjectType || "").trim(),
|
|
34
|
+
subjectId: String(binding?.subjectId || "").trim(),
|
|
35
|
+
role: String(binding?.role || "").trim(),
|
|
36
|
+
inherit: binding?.inherit !== false,
|
|
37
|
+
createdBy: binding?.createdBy ? String(binding.createdBy).trim() : null,
|
|
38
|
+
createdAt: Number(binding?.createdAt) || nowMs(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resourceKey(resourceType, resourceExternalId) {
|
|
43
|
+
return `${String(resourceType)}:${String(resourceExternalId)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
if (!raw.trim()) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
} catch (_error) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeJson(filePath, payload) {
|
|
62
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class FileAclStore {
|
|
67
|
+
constructor(storageRootDir) {
|
|
68
|
+
this.filePath = path.join(storageRootDir, "acl.json");
|
|
69
|
+
this.cache = {
|
|
70
|
+
resources: [],
|
|
71
|
+
bindings: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async init() {
|
|
76
|
+
const payload = readJson(this.filePath);
|
|
77
|
+
if (payload && typeof payload === "object") {
|
|
78
|
+
this.cache.resources = Array.isArray(payload.resources)
|
|
79
|
+
? payload.resources.map(normalizeResource)
|
|
80
|
+
: [];
|
|
81
|
+
this.cache.bindings = Array.isArray(payload.bindings)
|
|
82
|
+
? payload.bindings.map(normalizeBinding)
|
|
83
|
+
: [];
|
|
84
|
+
}
|
|
85
|
+
await this.flush();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async flush() {
|
|
89
|
+
writeJson(this.filePath, {
|
|
90
|
+
resources: this.cache.resources,
|
|
91
|
+
bindings: this.cache.bindings,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async close() {
|
|
96
|
+
// noop
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async listResources() {
|
|
100
|
+
return this.cache.resources.map((resource) => ({ ...resource }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async replaceResources(resources) {
|
|
104
|
+
const normalized = Array.isArray(resources) ? resources.map(normalizeResource) : [];
|
|
105
|
+
const filtered = normalized.filter((resource) => resource.type && resource.externalId);
|
|
106
|
+
const existingKeys = new Set(filtered.map((resource) => resourceKey(resource.type, resource.externalId)));
|
|
107
|
+
|
|
108
|
+
this.cache.resources = filtered;
|
|
109
|
+
this.cache.bindings = this.cache.bindings.filter((binding) =>
|
|
110
|
+
existingKeys.has(resourceKey(binding.resourceType, binding.resourceExternalId)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await this.flush();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async listBindings() {
|
|
117
|
+
return this.cache.bindings.map((binding) => ({ ...binding }));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async listBindingsForResource(resourceType, resourceExternalId) {
|
|
121
|
+
return this.cache.bindings
|
|
122
|
+
.filter(
|
|
123
|
+
(binding) =>
|
|
124
|
+
binding.resourceType === resourceType &&
|
|
125
|
+
binding.resourceExternalId === resourceExternalId,
|
|
126
|
+
)
|
|
127
|
+
.map((binding) => ({ ...binding }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async hasBindings() {
|
|
131
|
+
return this.cache.bindings.length > 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async putBinding(binding) {
|
|
135
|
+
const normalized = normalizeBinding(binding);
|
|
136
|
+
const key = resourceKey(normalized.resourceType, normalized.resourceExternalId);
|
|
137
|
+
const resourceExists = this.cache.resources.some(
|
|
138
|
+
(resource) => resourceKey(resource.type, resource.externalId) === key,
|
|
139
|
+
);
|
|
140
|
+
if (!resourceExists) {
|
|
141
|
+
throw new Error("Resource not found for ACL binding.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nextBindings = this.cache.bindings.filter((item) => item.id !== normalized.id);
|
|
145
|
+
nextBindings.push(normalized);
|
|
146
|
+
this.cache.bindings = nextBindings;
|
|
147
|
+
await this.flush();
|
|
148
|
+
return { ...normalized };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteBinding(bindingId) {
|
|
152
|
+
const id = String(bindingId || "").trim();
|
|
153
|
+
const previousLength = this.cache.bindings.length;
|
|
154
|
+
this.cache.bindings = this.cache.bindings.filter((binding) => binding.id !== id);
|
|
155
|
+
const deleted = previousLength !== this.cache.bindings.length;
|
|
156
|
+
if (deleted) {
|
|
157
|
+
await this.flush();
|
|
158
|
+
}
|
|
159
|
+
return deleted;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class PostgresAclStore {
|
|
164
|
+
constructor(connectionString) {
|
|
165
|
+
this.connectionString = connectionString;
|
|
166
|
+
this.pool = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async init() {
|
|
170
|
+
const { Pool } = await import("pg");
|
|
171
|
+
this.pool = new Pool({ connectionString: this.connectionString });
|
|
172
|
+
|
|
173
|
+
await this.pool.query(`
|
|
174
|
+
CREATE TABLE IF NOT EXISTS acl_resources (
|
|
175
|
+
type TEXT NOT NULL,
|
|
176
|
+
external_id TEXT NOT NULL,
|
|
177
|
+
parent_type TEXT,
|
|
178
|
+
parent_external_id TEXT,
|
|
179
|
+
owner_subject TEXT,
|
|
180
|
+
created_at BIGINT NOT NULL,
|
|
181
|
+
updated_at BIGINT NOT NULL,
|
|
182
|
+
PRIMARY KEY (type, external_id)
|
|
183
|
+
)
|
|
184
|
+
`);
|
|
185
|
+
|
|
186
|
+
await this.pool.query(`
|
|
187
|
+
CREATE TABLE IF NOT EXISTS acl_bindings (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
resource_type TEXT NOT NULL,
|
|
190
|
+
resource_external_id TEXT NOT NULL,
|
|
191
|
+
subject_type TEXT NOT NULL,
|
|
192
|
+
subject_id TEXT NOT NULL,
|
|
193
|
+
role TEXT NOT NULL,
|
|
194
|
+
inherit BOOLEAN NOT NULL DEFAULT TRUE,
|
|
195
|
+
created_by TEXT,
|
|
196
|
+
created_at BIGINT NOT NULL
|
|
197
|
+
)
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
await this.pool.query(
|
|
201
|
+
"CREATE INDEX IF NOT EXISTS acl_bindings_resource_idx ON acl_bindings (resource_type, resource_external_id)",
|
|
202
|
+
);
|
|
203
|
+
await this.pool.query(
|
|
204
|
+
"CREATE INDEX IF NOT EXISTS acl_bindings_subject_idx ON acl_bindings (subject_type, subject_id)",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async close() {
|
|
209
|
+
if (this.pool) {
|
|
210
|
+
await this.pool.end();
|
|
211
|
+
this.pool = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async listResources() {
|
|
216
|
+
const result = await this.pool.query(
|
|
217
|
+
"SELECT type, external_id, parent_type, parent_external_id, owner_subject, created_at, updated_at FROM acl_resources",
|
|
218
|
+
);
|
|
219
|
+
return result.rows.map((row) =>
|
|
220
|
+
normalizeResource({
|
|
221
|
+
type: row.type,
|
|
222
|
+
externalId: row.external_id,
|
|
223
|
+
parentType: row.parent_type,
|
|
224
|
+
parentExternalId: row.parent_external_id,
|
|
225
|
+
ownerSubject: row.owner_subject,
|
|
226
|
+
createdAt: row.created_at,
|
|
227
|
+
updatedAt: row.updated_at,
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async replaceResources(resources) {
|
|
233
|
+
const client = await this.pool.connect();
|
|
234
|
+
try {
|
|
235
|
+
await client.query("BEGIN");
|
|
236
|
+
await client.query("DELETE FROM acl_resources");
|
|
237
|
+
|
|
238
|
+
const normalized = Array.isArray(resources) ? resources.map(normalizeResource) : [];
|
|
239
|
+
for (const resource of normalized) {
|
|
240
|
+
if (!resource.type || !resource.externalId) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
await client.query(
|
|
244
|
+
`
|
|
245
|
+
INSERT INTO acl_resources (
|
|
246
|
+
type,
|
|
247
|
+
external_id,
|
|
248
|
+
parent_type,
|
|
249
|
+
parent_external_id,
|
|
250
|
+
owner_subject,
|
|
251
|
+
created_at,
|
|
252
|
+
updated_at
|
|
253
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
254
|
+
`,
|
|
255
|
+
[
|
|
256
|
+
resource.type,
|
|
257
|
+
resource.externalId,
|
|
258
|
+
resource.parentType,
|
|
259
|
+
resource.parentExternalId,
|
|
260
|
+
resource.ownerSubject,
|
|
261
|
+
resource.createdAt,
|
|
262
|
+
resource.updatedAt,
|
|
263
|
+
],
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await client.query(`
|
|
268
|
+
DELETE FROM acl_bindings b
|
|
269
|
+
WHERE NOT EXISTS (
|
|
270
|
+
SELECT 1
|
|
271
|
+
FROM acl_resources r
|
|
272
|
+
WHERE r.type = b.resource_type
|
|
273
|
+
AND r.external_id = b.resource_external_id
|
|
274
|
+
)
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
await client.query("COMMIT");
|
|
278
|
+
} catch (error) {
|
|
279
|
+
await client.query("ROLLBACK");
|
|
280
|
+
throw error;
|
|
281
|
+
} finally {
|
|
282
|
+
client.release();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async listBindings() {
|
|
287
|
+
const result = await this.pool.query(
|
|
288
|
+
"SELECT id, resource_type, resource_external_id, subject_type, subject_id, role, inherit, created_by, created_at FROM acl_bindings",
|
|
289
|
+
);
|
|
290
|
+
return result.rows.map((row) =>
|
|
291
|
+
normalizeBinding({
|
|
292
|
+
id: row.id,
|
|
293
|
+
resourceType: row.resource_type,
|
|
294
|
+
resourceExternalId: row.resource_external_id,
|
|
295
|
+
subjectType: row.subject_type,
|
|
296
|
+
subjectId: row.subject_id,
|
|
297
|
+
role: row.role,
|
|
298
|
+
inherit: row.inherit,
|
|
299
|
+
createdBy: row.created_by,
|
|
300
|
+
createdAt: row.created_at,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async listBindingsForResource(resourceType, resourceExternalId) {
|
|
306
|
+
const result = await this.pool.query(
|
|
307
|
+
`
|
|
308
|
+
SELECT id, resource_type, resource_external_id, subject_type, subject_id, role, inherit, created_by, created_at
|
|
309
|
+
FROM acl_bindings
|
|
310
|
+
WHERE resource_type = $1 AND resource_external_id = $2
|
|
311
|
+
`,
|
|
312
|
+
[resourceType, resourceExternalId],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return result.rows.map((row) =>
|
|
316
|
+
normalizeBinding({
|
|
317
|
+
id: row.id,
|
|
318
|
+
resourceType: row.resource_type,
|
|
319
|
+
resourceExternalId: row.resource_external_id,
|
|
320
|
+
subjectType: row.subject_type,
|
|
321
|
+
subjectId: row.subject_id,
|
|
322
|
+
role: row.role,
|
|
323
|
+
inherit: row.inherit,
|
|
324
|
+
createdBy: row.created_by,
|
|
325
|
+
createdAt: row.created_at,
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async hasBindings() {
|
|
331
|
+
const result = await this.pool.query("SELECT 1 FROM acl_bindings LIMIT 1");
|
|
332
|
+
return result.rows.length > 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async putBinding(binding) {
|
|
336
|
+
const normalized = normalizeBinding(binding);
|
|
337
|
+
const resourceResult = await this.pool.query(
|
|
338
|
+
"SELECT 1 FROM acl_resources WHERE type = $1 AND external_id = $2",
|
|
339
|
+
[normalized.resourceType, normalized.resourceExternalId],
|
|
340
|
+
);
|
|
341
|
+
if (!resourceResult.rows.length) {
|
|
342
|
+
throw new Error("Resource not found for ACL binding.");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await this.pool.query(
|
|
346
|
+
`
|
|
347
|
+
INSERT INTO acl_bindings (
|
|
348
|
+
id,
|
|
349
|
+
resource_type,
|
|
350
|
+
resource_external_id,
|
|
351
|
+
subject_type,
|
|
352
|
+
subject_id,
|
|
353
|
+
role,
|
|
354
|
+
inherit,
|
|
355
|
+
created_by,
|
|
356
|
+
created_at
|
|
357
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
|
358
|
+
ON CONFLICT (id)
|
|
359
|
+
DO UPDATE SET
|
|
360
|
+
resource_type = EXCLUDED.resource_type,
|
|
361
|
+
resource_external_id = EXCLUDED.resource_external_id,
|
|
362
|
+
subject_type = EXCLUDED.subject_type,
|
|
363
|
+
subject_id = EXCLUDED.subject_id,
|
|
364
|
+
role = EXCLUDED.role,
|
|
365
|
+
inherit = EXCLUDED.inherit,
|
|
366
|
+
created_by = EXCLUDED.created_by,
|
|
367
|
+
created_at = EXCLUDED.created_at
|
|
368
|
+
`,
|
|
369
|
+
[
|
|
370
|
+
normalized.id,
|
|
371
|
+
normalized.resourceType,
|
|
372
|
+
normalized.resourceExternalId,
|
|
373
|
+
normalized.subjectType,
|
|
374
|
+
normalized.subjectId,
|
|
375
|
+
normalized.role,
|
|
376
|
+
normalized.inherit,
|
|
377
|
+
normalized.createdBy,
|
|
378
|
+
normalized.createdAt,
|
|
379
|
+
],
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
return normalized;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async deleteBinding(bindingId) {
|
|
386
|
+
const result = await this.pool.query("DELETE FROM acl_bindings WHERE id = $1", [bindingId]);
|
|
387
|
+
return result.rowCount > 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function createAclStore({ storageRootDir }) {
|
|
392
|
+
const connectionString = String(process.env.TUI_NOTES_AUTH_DB_URL || "").trim();
|
|
393
|
+
const store = connectionString
|
|
394
|
+
? new PostgresAclStore(connectionString)
|
|
395
|
+
: new FileAclStore(storageRootDir);
|
|
396
|
+
|
|
397
|
+
await store.init();
|
|
398
|
+
return store;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function createBindingInput({
|
|
402
|
+
resourceType,
|
|
403
|
+
resourceExternalId,
|
|
404
|
+
subjectType,
|
|
405
|
+
subjectId,
|
|
406
|
+
role,
|
|
407
|
+
inherit,
|
|
408
|
+
createdBy,
|
|
409
|
+
id,
|
|
410
|
+
}) {
|
|
411
|
+
return normalizeBinding({
|
|
412
|
+
id,
|
|
413
|
+
resourceType,
|
|
414
|
+
resourceExternalId,
|
|
415
|
+
subjectType,
|
|
416
|
+
subjectId,
|
|
417
|
+
role,
|
|
418
|
+
inherit,
|
|
419
|
+
createdBy,
|
|
420
|
+
});
|
|
421
|
+
}
|
package/server/auth.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const AUTH_MODES = new Set(["off", "observe", "enforce"]);
|
|
2
|
+
|
|
3
|
+
function normalizeAuthMode(value) {
|
|
4
|
+
const mode = String(value || "").trim().toLowerCase();
|
|
5
|
+
return AUTH_MODES.has(mode) ? mode : "off";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getAuthMode() {
|
|
9
|
+
return normalizeAuthMode(process.env.TUI_NOTES_AUTH_MODE || "off");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseGroups(input) {
|
|
13
|
+
if (Array.isArray(input)) {
|
|
14
|
+
return [...new Set(input.map((item) => String(item || "").trim()).filter(Boolean))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return [...new Set(
|
|
18
|
+
String(input || "")
|
|
19
|
+
.split(/[;,\n]/)
|
|
20
|
+
.map((chunk) => chunk.trim())
|
|
21
|
+
.filter(Boolean),
|
|
22
|
+
)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readHeader(req, names) {
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
const value = req.headers?.[name];
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const first = value.find((entry) => String(entry || "").trim());
|
|
30
|
+
if (first) {
|
|
31
|
+
return String(first).trim();
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string" && value.trim()) {
|
|
36
|
+
return value.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getIdentityFromHeaders(req) {
|
|
43
|
+
const userId =
|
|
44
|
+
readHeader(req, ["x-auth-request-user", "x-forwarded-user", "x-user-id", "x-user"]) || "";
|
|
45
|
+
const email =
|
|
46
|
+
readHeader(req, ["x-auth-request-email", "x-forwarded-email", "x-user-email", "x-email"]) || "";
|
|
47
|
+
const preferredUsername =
|
|
48
|
+
readHeader(
|
|
49
|
+
req,
|
|
50
|
+
["x-auth-request-preferred-username", "x-preferred-username", "x-user-name", "x-username"],
|
|
51
|
+
) || "";
|
|
52
|
+
const groupsRaw =
|
|
53
|
+
readHeader(req, ["x-auth-request-groups", "x-forwarded-groups", "x-user-groups", "x-groups"]) || "";
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
userId,
|
|
57
|
+
email,
|
|
58
|
+
preferredUsername,
|
|
59
|
+
groups: parseGroups(groupsRaw),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeIdentity(identity) {
|
|
64
|
+
const userId = String(identity?.userId || "").trim();
|
|
65
|
+
const email = String(identity?.email || "").trim().toLowerCase();
|
|
66
|
+
const preferredUsername = String(identity?.preferredUsername || "").trim();
|
|
67
|
+
const groups = parseGroups(identity?.groups);
|
|
68
|
+
const emailLocalPart = email.includes("@") ? email.split("@")[0].trim() : email;
|
|
69
|
+
|
|
70
|
+
const isAuthenticated = Boolean(userId || email || preferredUsername);
|
|
71
|
+
const stableId = email || preferredUsername || userId || "anonymous";
|
|
72
|
+
const displayName =
|
|
73
|
+
preferredUsername ||
|
|
74
|
+
emailLocalPart ||
|
|
75
|
+
userId ||
|
|
76
|
+
stableId ||
|
|
77
|
+
"anonymous";
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: stableId,
|
|
81
|
+
userId,
|
|
82
|
+
email,
|
|
83
|
+
preferredUsername,
|
|
84
|
+
groups,
|
|
85
|
+
isAuthenticated,
|
|
86
|
+
displayName,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createAuthContext(req) {
|
|
91
|
+
const mode = getAuthMode();
|
|
92
|
+
const identity = normalizeIdentity(getIdentityFromHeaders(req));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
mode,
|
|
96
|
+
user: identity,
|
|
97
|
+
isAuthenticated: identity.isAuthenticated,
|
|
98
|
+
isEnforced: mode === "enforce",
|
|
99
|
+
isObserve: mode === "observe",
|
|
100
|
+
isOff: mode === "off",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function authContextMiddleware(req, _res, next) {
|
|
105
|
+
req.authContext = createAuthContext(req);
|
|
106
|
+
next();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function requireAuthentication(req, res, next) {
|
|
110
|
+
const context = req.authContext || createAuthContext(req);
|
|
111
|
+
req.authContext = context;
|
|
112
|
+
|
|
113
|
+
if (context.isEnforced && !context.isAuthenticated) {
|
|
114
|
+
res.status(401).json({ message: "Authentication required." });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
next();
|
|
119
|
+
}
|