@nest-admin/nestjs 0.11.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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/admin-ui/assets/index-AyWOamlt.js +50 -0
- package/dist/admin-ui/assets/index-AyWOamlt.js.map +1 -0
- package/dist/admin-ui/assets/index-D4Eh84eD.css +2 -0
- package/dist/admin-ui/index.html +14 -0
- package/dist/chunk-7IXLRGGQ.js +356 -0
- package/dist/chunk-7IXLRGGQ.js.map +1 -0
- package/dist/drizzle.cjs +895 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +335 -0
- package/dist/drizzle.d.ts +335 -0
- package/dist/drizzle.js +756 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/index.cjs +3247 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1652 -0
- package/dist/index.d.ts +1652 -0
- package/dist/index.js +2901 -0
- package/dist/index.js.map +1 -0
- package/dist/prisma.cjs +1159 -0
- package/dist/prisma.cjs.map +1 -0
- package/dist/prisma.d.cts +585 -0
- package/dist/prisma.d.ts +585 -0
- package/dist/prisma.js +995 -0
- package/dist/prisma.js.map +1 -0
- package/package.json +130 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
AdapterError: () => AdapterError,
|
|
25
|
+
AdminModule: () => AdminModule,
|
|
26
|
+
ConstraintError: () => ConstraintError,
|
|
27
|
+
FieldNotFoundError: () => FieldNotFoundError,
|
|
28
|
+
ForbiddenError: () => ForbiddenError,
|
|
29
|
+
InvalidQueryError: () => InvalidQueryError,
|
|
30
|
+
ModelNotFoundError: () => ModelNotFoundError,
|
|
31
|
+
NestAdminError: () => NestAdminError,
|
|
32
|
+
RecordNotFoundError: () => RecordNotFoundError,
|
|
33
|
+
UnauthorizedError: () => UnauthorizedError,
|
|
34
|
+
ValidationError: () => ValidationError,
|
|
35
|
+
adminAccountOf: () => adminAccountOf,
|
|
36
|
+
builtInAuth: () => builtInAuth,
|
|
37
|
+
generateSessionSecret: () => generateSessionSecret,
|
|
38
|
+
hashAdminPassword: () => hashAdminPassword,
|
|
39
|
+
isNestAdminError: () => isNestAdminError,
|
|
40
|
+
unsafeAllowAllRequests: () => unsafeAllowAllRequests,
|
|
41
|
+
verifyAdminPassword: () => verifyAdminPassword
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(src_exports);
|
|
44
|
+
|
|
45
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.1_64048c8b0f36b1e1c1865447e0ade244/node_modules/tsup/assets/cjs_shims.js
|
|
46
|
+
var getImportMetaUrl = /* @__PURE__ */ __name(() => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href, "getImportMetaUrl");
|
|
47
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
48
|
+
|
|
49
|
+
// src/module.ts
|
|
50
|
+
var import_common11 = require("@nestjs/common");
|
|
51
|
+
var import_core10 = require("@nestjs/core");
|
|
52
|
+
|
|
53
|
+
// ../core/dist/index.js
|
|
54
|
+
function summarise(account) {
|
|
55
|
+
return {
|
|
56
|
+
id: account.id,
|
|
57
|
+
email: account.email,
|
|
58
|
+
...account.name !== void 0 ? {
|
|
59
|
+
name: account.name
|
|
60
|
+
} : {}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
__name(summarise, "summarise");
|
|
64
|
+
var CREATED = [
|
|
65
|
+
"createdAt",
|
|
66
|
+
"created_at",
|
|
67
|
+
"created",
|
|
68
|
+
"createdOn",
|
|
69
|
+
"insertedAt",
|
|
70
|
+
"inserted_at"
|
|
71
|
+
];
|
|
72
|
+
var NOT_CREATED = [
|
|
73
|
+
"updated",
|
|
74
|
+
"modified",
|
|
75
|
+
"deleted",
|
|
76
|
+
"archived",
|
|
77
|
+
"expires",
|
|
78
|
+
"expired"
|
|
79
|
+
];
|
|
80
|
+
function isDate(field) {
|
|
81
|
+
return field.kind === "datetime" && !field.isList && !field.relation;
|
|
82
|
+
}
|
|
83
|
+
__name(isDate, "isDate");
|
|
84
|
+
function excluded(name) {
|
|
85
|
+
const lower = name.toLowerCase();
|
|
86
|
+
return NOT_CREATED.some((word) => lower.startsWith(word));
|
|
87
|
+
}
|
|
88
|
+
__name(excluded, "excluded");
|
|
89
|
+
function createdFieldFor(model) {
|
|
90
|
+
const dates = model.fields.filter(isDate);
|
|
91
|
+
for (const conventional of CREATED) {
|
|
92
|
+
const match = dates.find((field) => field.name.toLowerCase() === conventional.toLowerCase());
|
|
93
|
+
if (match) return match.name;
|
|
94
|
+
}
|
|
95
|
+
const generated = dates.filter((field) => field.isGenerated && !excluded(field.name));
|
|
96
|
+
return generated.length === 1 ? generated[0]?.name : void 0;
|
|
97
|
+
}
|
|
98
|
+
__name(createdFieldFor, "createdFieldFor");
|
|
99
|
+
var CONVENTIONAL = [
|
|
100
|
+
"name",
|
|
101
|
+
"title",
|
|
102
|
+
"label",
|
|
103
|
+
"displayName",
|
|
104
|
+
"username",
|
|
105
|
+
"email",
|
|
106
|
+
"slug"
|
|
107
|
+
];
|
|
108
|
+
function isReadable(field) {
|
|
109
|
+
return field.kind === "string" && !field.isList && !field.relation && // A generated string is a cuid or a uuid: readable characters, no meaning.
|
|
110
|
+
!field.isGenerated;
|
|
111
|
+
}
|
|
112
|
+
__name(isReadable, "isReadable");
|
|
113
|
+
function displayFieldFor(model) {
|
|
114
|
+
if (model.displayField !== void 0) return model.displayField;
|
|
115
|
+
const readable2 = model.fields.filter(isReadable);
|
|
116
|
+
for (const candidate of CONVENTIONAL) {
|
|
117
|
+
const match = readable2.find((field) => field.name === candidate);
|
|
118
|
+
if (match) return match.name;
|
|
119
|
+
}
|
|
120
|
+
const unique = readable2.find((field) => field.isUnique && !field.isId);
|
|
121
|
+
if (unique) return unique.name;
|
|
122
|
+
const plain = readable2.find((field) => !field.isId);
|
|
123
|
+
if (plain) return plain.name;
|
|
124
|
+
return model.primaryKey[0] ?? model.fields[0]?.name ?? "id";
|
|
125
|
+
}
|
|
126
|
+
__name(displayFieldFor, "displayFieldFor");
|
|
127
|
+
function inverseRelationField(field, models) {
|
|
128
|
+
const relation = field.relation;
|
|
129
|
+
if (!relation?.name) return void 0;
|
|
130
|
+
const target = models.find((model) => model.name === relation.targetModel);
|
|
131
|
+
if (!target) return void 0;
|
|
132
|
+
return target.fields.find((candidate) => candidate.relation?.name === relation.name && candidate !== field);
|
|
133
|
+
}
|
|
134
|
+
__name(inverseRelationField, "inverseRelationField");
|
|
135
|
+
function relationShape(field, models) {
|
|
136
|
+
const relation = field.relation;
|
|
137
|
+
if (!relation) return void 0;
|
|
138
|
+
if (relation.cardinality === "one") return "to-one";
|
|
139
|
+
const inverse = inverseRelationField(field, models);
|
|
140
|
+
return inverse?.relation?.cardinality === "many" ? "many-to-many" : "one-to-many";
|
|
141
|
+
}
|
|
142
|
+
__name(relationShape, "relationShape");
|
|
143
|
+
function detachBlockedReason(field, models) {
|
|
144
|
+
if (relationShape(field, models) !== "one-to-many") return void 0;
|
|
145
|
+
const inverse = inverseRelationField(field, models);
|
|
146
|
+
if (!inverse?.isRequired) return void 0;
|
|
147
|
+
const target = field.relation?.targetModel ?? "the related model";
|
|
148
|
+
return `${target}.${inverse.name} is required, so a ${target} record cannot exist without one. Delete the record, or point it at something else, instead of detaching it.`;
|
|
149
|
+
}
|
|
150
|
+
__name(detachBlockedReason, "detachBlockedReason");
|
|
151
|
+
function selectModels(models, selection) {
|
|
152
|
+
if (!selection) return models;
|
|
153
|
+
const included = selection.include ? new Set(selection.include) : void 0;
|
|
154
|
+
const excluded2 = new Set(selection.exclude ?? []);
|
|
155
|
+
return models.filter((model) => (included === void 0 || included.has(model.name)) && !excluded2.has(model.name));
|
|
156
|
+
}
|
|
157
|
+
__name(selectModels, "selectModels");
|
|
158
|
+
function unknownSelectionNames(models, selection) {
|
|
159
|
+
if (!selection) return [];
|
|
160
|
+
const known = new Set(models.map((model) => model.name));
|
|
161
|
+
const referenced = [
|
|
162
|
+
...selection.include ?? [],
|
|
163
|
+
...selection.exclude ?? []
|
|
164
|
+
];
|
|
165
|
+
return [
|
|
166
|
+
...new Set(referenced.filter((name) => !known.has(name)))
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
__name(unknownSelectionNames, "unknownSelectionNames");
|
|
170
|
+
function fieldOverride(overrides, model, field) {
|
|
171
|
+
return overrides?.[model]?.fields?.[field];
|
|
172
|
+
}
|
|
173
|
+
__name(fieldOverride, "fieldOverride");
|
|
174
|
+
function isReadOnly(overrides, model, field) {
|
|
175
|
+
return field.isGenerated || fieldOverride(overrides, model, field.name)?.readOnly === true;
|
|
176
|
+
}
|
|
177
|
+
__name(isReadOnly, "isReadOnly");
|
|
178
|
+
function applyOverrides(models, overrides) {
|
|
179
|
+
if (!overrides) return models;
|
|
180
|
+
return models.map((model) => {
|
|
181
|
+
const override = overrides[model.name];
|
|
182
|
+
if (!override) return model;
|
|
183
|
+
const hidden = new Set(Object.entries(override.fields ?? {}).filter(([, field]) => field.hidden === true).map(([name]) => name));
|
|
184
|
+
const writeOnly = new Set(Object.entries(override.fields ?? {}).filter(([, field]) => field.writeOnly === true).map(([name]) => name));
|
|
185
|
+
const kept = hidden.size === 0 ? model.fields : model.fields.filter((f) => !hidden.has(f.name));
|
|
186
|
+
return {
|
|
187
|
+
...model,
|
|
188
|
+
...override.displayField !== void 0 ? {
|
|
189
|
+
displayField: override.displayField
|
|
190
|
+
} : {},
|
|
191
|
+
// Carried onto the metadata rather than looked up again later, so
|
|
192
|
+
// everything downstream - the field scope, the projection, the DTO -
|
|
193
|
+
// reads one flag instead of each re-deriving it from the configuration.
|
|
194
|
+
fields: writeOnly.size === 0 ? kept : kept.map((field) => writeOnly.has(field.name) ? {
|
|
195
|
+
...field,
|
|
196
|
+
writeOnly: true
|
|
197
|
+
} : field)
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
__name(applyOverrides, "applyOverrides");
|
|
202
|
+
function unknownOverrideNames(models, overrides) {
|
|
203
|
+
if (!overrides) return [];
|
|
204
|
+
const unknown = [];
|
|
205
|
+
for (const [modelName, override] of Object.entries(overrides)) {
|
|
206
|
+
const model = models.find((candidate) => candidate.name === modelName);
|
|
207
|
+
if (!model) {
|
|
208
|
+
unknown.push(modelName);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const names = new Set(model.fields.map((field) => field.name));
|
|
212
|
+
if (override.displayField !== void 0 && !names.has(override.displayField)) {
|
|
213
|
+
unknown.push(`${modelName}.${override.displayField}`);
|
|
214
|
+
}
|
|
215
|
+
for (const fieldName of Object.keys(override.fields ?? {})) {
|
|
216
|
+
if (!names.has(fieldName)) unknown.push(`${modelName}.${fieldName}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return unknown;
|
|
220
|
+
}
|
|
221
|
+
__name(unknownOverrideNames, "unknownOverrideNames");
|
|
222
|
+
function unwritableHiddenFields(models, overrides) {
|
|
223
|
+
if (!overrides) return [];
|
|
224
|
+
const blocked = [];
|
|
225
|
+
for (const [modelName, override] of Object.entries(overrides)) {
|
|
226
|
+
const model = models.find((candidate) => candidate.name === modelName);
|
|
227
|
+
if (!model) continue;
|
|
228
|
+
for (const [fieldName, field] of Object.entries(override.fields ?? {})) {
|
|
229
|
+
if (field.hidden !== true) continue;
|
|
230
|
+
const declared = model.fields.find((candidate) => candidate.name === fieldName);
|
|
231
|
+
if (!declared) continue;
|
|
232
|
+
if (declared.isRequired && !declared.isGenerated && declared.defaultValue === void 0) {
|
|
233
|
+
blocked.push(`${modelName}.${fieldName}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return blocked;
|
|
238
|
+
}
|
|
239
|
+
__name(unwritableHiddenFields, "unwritableHiddenFields");
|
|
240
|
+
var BRAND = /* @__PURE__ */ Symbol.for("nest-admin.error");
|
|
241
|
+
var NestAdminError = class extends Error {
|
|
242
|
+
static {
|
|
243
|
+
__name(this, "NestAdminError");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Which error this is.
|
|
247
|
+
*
|
|
248
|
+
* Subclasses override it with a literal. The base value covers anything that
|
|
249
|
+
* extends this class without declaring one - the Prisma schema errors, for
|
|
250
|
+
* instance - which the transport layer treats as internal.
|
|
251
|
+
*/
|
|
252
|
+
kind = "unknown";
|
|
253
|
+
constructor(message, options) {
|
|
254
|
+
super(message, options);
|
|
255
|
+
this.name = new.target.name;
|
|
256
|
+
Object.defineProperty(this, BRAND, {
|
|
257
|
+
value: true,
|
|
258
|
+
enumerable: false
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function isNestAdminError(value) {
|
|
263
|
+
return typeof value === "object" && value !== null && value[BRAND] === true;
|
|
264
|
+
}
|
|
265
|
+
__name(isNestAdminError, "isNestAdminError");
|
|
266
|
+
var ModelNotFoundError = class extends NestAdminError {
|
|
267
|
+
static {
|
|
268
|
+
__name(this, "ModelNotFoundError");
|
|
269
|
+
}
|
|
270
|
+
constructor(model, availableModels = []) {
|
|
271
|
+
const known = availableModels.length > 0 ? ` Known models: ${availableModels.join(", ")}.` : "";
|
|
272
|
+
super(`Unknown model "${model}".${known}`);
|
|
273
|
+
this.model = model;
|
|
274
|
+
this.availableModels = availableModels;
|
|
275
|
+
}
|
|
276
|
+
model;
|
|
277
|
+
availableModels;
|
|
278
|
+
kind = "model-not-found";
|
|
279
|
+
};
|
|
280
|
+
var FieldNotFoundError = class extends NestAdminError {
|
|
281
|
+
static {
|
|
282
|
+
__name(this, "FieldNotFoundError");
|
|
283
|
+
}
|
|
284
|
+
constructor(model, field, reason) {
|
|
285
|
+
super(`Unknown field "${field}" on model "${model}".${reason ? ` ${reason}` : ""}`);
|
|
286
|
+
this.model = model;
|
|
287
|
+
this.field = field;
|
|
288
|
+
}
|
|
289
|
+
model;
|
|
290
|
+
field;
|
|
291
|
+
kind = "field-not-found";
|
|
292
|
+
};
|
|
293
|
+
var RecordNotFoundError = class extends NestAdminError {
|
|
294
|
+
static {
|
|
295
|
+
__name(this, "RecordNotFoundError");
|
|
296
|
+
}
|
|
297
|
+
constructor(model, id) {
|
|
298
|
+
super(`No ${model} record found for id ${JSON.stringify(id)}.`);
|
|
299
|
+
this.model = model;
|
|
300
|
+
this.id = id;
|
|
301
|
+
}
|
|
302
|
+
model;
|
|
303
|
+
id;
|
|
304
|
+
kind = "record-not-found";
|
|
305
|
+
};
|
|
306
|
+
var InvalidQueryError = class extends NestAdminError {
|
|
307
|
+
static {
|
|
308
|
+
__name(this, "InvalidQueryError");
|
|
309
|
+
}
|
|
310
|
+
kind = "invalid-query";
|
|
311
|
+
};
|
|
312
|
+
var ValidationError = class extends NestAdminError {
|
|
313
|
+
static {
|
|
314
|
+
__name(this, "ValidationError");
|
|
315
|
+
}
|
|
316
|
+
constructor(message, fields = [], options) {
|
|
317
|
+
super(message, options);
|
|
318
|
+
this.fields = fields;
|
|
319
|
+
}
|
|
320
|
+
fields;
|
|
321
|
+
kind = "validation";
|
|
322
|
+
};
|
|
323
|
+
var ConstraintError = class extends NestAdminError {
|
|
324
|
+
static {
|
|
325
|
+
__name(this, "ConstraintError");
|
|
326
|
+
}
|
|
327
|
+
constructor(constraint, model, fields = []) {
|
|
328
|
+
super(describeConstraint(constraint, model, fields));
|
|
329
|
+
this.constraint = constraint;
|
|
330
|
+
this.model = model;
|
|
331
|
+
this.fields = fields;
|
|
332
|
+
}
|
|
333
|
+
constraint;
|
|
334
|
+
model;
|
|
335
|
+
fields;
|
|
336
|
+
kind = "constraint";
|
|
337
|
+
};
|
|
338
|
+
function describeConstraint(constraint, model, fields) {
|
|
339
|
+
const named = fields.length > 0 ? fields.join(", ") : void 0;
|
|
340
|
+
switch (constraint) {
|
|
341
|
+
case "unique":
|
|
342
|
+
return named ? `Another ${model} already has this ${named}.` : `Another ${model} already has one of these values.`;
|
|
343
|
+
case "foreign-key":
|
|
344
|
+
return named ? `The ${named} does not refer to an existing record, or the record it refers to is still in use.` : `A reference on this ${model} does not point at an existing record, or is still in use.`;
|
|
345
|
+
case "required":
|
|
346
|
+
return named ? `${named} is required.` : `A required value on this ${model} is missing.`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
__name(describeConstraint, "describeConstraint");
|
|
350
|
+
var AdapterError = class extends NestAdminError {
|
|
351
|
+
static {
|
|
352
|
+
__name(this, "AdapterError");
|
|
353
|
+
}
|
|
354
|
+
kind = "adapter";
|
|
355
|
+
constructor(message, options) {
|
|
356
|
+
super(message, options);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var UnauthorizedError = class extends NestAdminError {
|
|
360
|
+
static {
|
|
361
|
+
__name(this, "UnauthorizedError");
|
|
362
|
+
}
|
|
363
|
+
kind = "unauthorized";
|
|
364
|
+
constructor(message = "Authentication is required to access the admin API.") {
|
|
365
|
+
super(message);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var ForbiddenError = class extends NestAdminError {
|
|
369
|
+
static {
|
|
370
|
+
__name(this, "ForbiddenError");
|
|
371
|
+
}
|
|
372
|
+
kind = "forbidden";
|
|
373
|
+
constructor(message = "You do not have permission to access the admin API.") {
|
|
374
|
+
super(message);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/admin/controller.ts
|
|
379
|
+
var import_common7 = require("@nestjs/common");
|
|
380
|
+
|
|
381
|
+
// src/auth/guard.ts
|
|
382
|
+
var import_common = require("@nestjs/common");
|
|
383
|
+
|
|
384
|
+
// src/tokens.ts
|
|
385
|
+
var ADMIN_ADAPTER = /* @__PURE__ */ Symbol("NEST_ADMIN_ADAPTER");
|
|
386
|
+
var ADMIN_AUTH = /* @__PURE__ */ Symbol("NEST_ADMIN_AUTH");
|
|
387
|
+
var ADMIN_RESOURCE_AUTH = /* @__PURE__ */ Symbol("NEST_ADMIN_RESOURCE_AUTH");
|
|
388
|
+
var ADMIN_UI_ROOT = /* @__PURE__ */ Symbol("NEST_ADMIN_UI_ROOT");
|
|
389
|
+
var ADMIN_MOUNT_PATH = /* @__PURE__ */ Symbol("NEST_ADMIN_MOUNT_PATH");
|
|
390
|
+
var ADMIN_RESOURCES = /* @__PURE__ */ Symbol("NEST_ADMIN_RESOURCES");
|
|
391
|
+
var ADMIN_OPTIONS = /* @__PURE__ */ Symbol("NEST_ADMIN_OPTIONS");
|
|
392
|
+
var ADMIN_MODELS = /* @__PURE__ */ Symbol("NEST_ADMIN_MODELS");
|
|
393
|
+
var ADMIN_HOOKS = /* @__PURE__ */ Symbol("NEST_ADMIN_HOOKS");
|
|
394
|
+
var ADMIN_ACTIONS = /* @__PURE__ */ Symbol("NEST_ADMIN_ACTIONS");
|
|
395
|
+
var ADMIN_DASHBOARD = /* @__PURE__ */ Symbol("NEST_ADMIN_DASHBOARD");
|
|
396
|
+
var ADMIN_THEME = /* @__PURE__ */ Symbol("NEST_ADMIN_THEME");
|
|
397
|
+
|
|
398
|
+
// src/auth/guard.ts
|
|
399
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
400
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
401
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
402
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
403
|
+
} else {
|
|
404
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
405
|
+
if (d = decorators[i]) {
|
|
406
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
411
|
+
}
|
|
412
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
413
|
+
function _ts_metadata(metadataKey, metadataValue) {
|
|
414
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
415
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
419
|
+
function _ts_param(paramIndex, decorator) {
|
|
420
|
+
return function(target, key) {
|
|
421
|
+
decorator(target, key, paramIndex);
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
__name(_ts_param, "_ts_param");
|
|
425
|
+
var AdminAuthGuard = class {
|
|
426
|
+
static {
|
|
427
|
+
__name(this, "AdminAuthGuard");
|
|
428
|
+
}
|
|
429
|
+
auth;
|
|
430
|
+
constructor(auth) {
|
|
431
|
+
this.auth = auth;
|
|
432
|
+
}
|
|
433
|
+
async canActivate(context) {
|
|
434
|
+
const decision = await this.auth.authorize(context);
|
|
435
|
+
if (decision === false) {
|
|
436
|
+
throw new ForbiddenError();
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
AdminAuthGuard = _ts_decorate([
|
|
442
|
+
(0, import_common.Injectable)(),
|
|
443
|
+
_ts_param(0, (0, import_common.Inject)(ADMIN_AUTH)),
|
|
444
|
+
_ts_metadata("design:type", Function),
|
|
445
|
+
_ts_metadata("design:paramtypes", [
|
|
446
|
+
typeof AdminAuth === "undefined" ? Object : AdminAuth
|
|
447
|
+
])
|
|
448
|
+
], AdminAuthGuard);
|
|
449
|
+
|
|
450
|
+
// src/http/execution-context.ts
|
|
451
|
+
var import_common2 = require("@nestjs/common");
|
|
452
|
+
var AdminContext = (0, import_common2.createParamDecorator)((_data, context) => context);
|
|
453
|
+
|
|
454
|
+
// src/http/exception.filter.ts
|
|
455
|
+
var import_common3 = require("@nestjs/common");
|
|
456
|
+
|
|
457
|
+
// src/http/response.ts
|
|
458
|
+
function success(data) {
|
|
459
|
+
return {
|
|
460
|
+
success: true,
|
|
461
|
+
data
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
__name(success, "success");
|
|
465
|
+
function successPage(data, meta) {
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
data,
|
|
469
|
+
meta
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
__name(successPage, "successPage");
|
|
473
|
+
function failure(code, message, details) {
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: {
|
|
477
|
+
code,
|
|
478
|
+
message,
|
|
479
|
+
...details ? {
|
|
480
|
+
details
|
|
481
|
+
} : {}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
__name(failure, "failure");
|
|
486
|
+
|
|
487
|
+
// src/http/exception.filter.ts
|
|
488
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
489
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
490
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
491
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
492
|
+
} else {
|
|
493
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
494
|
+
if (d = decorators[i]) {
|
|
495
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
500
|
+
}
|
|
501
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
502
|
+
var INTERNAL = {
|
|
503
|
+
status: import_common3.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
504
|
+
code: "INTERNAL_ERROR",
|
|
505
|
+
message: "An internal error occurred while handling the request."
|
|
506
|
+
};
|
|
507
|
+
function mapError(error) {
|
|
508
|
+
if (!isNestAdminError(error)) return INTERNAL;
|
|
509
|
+
switch (error.kind) {
|
|
510
|
+
// Auth first. No `details` on either: echoing anything about why a request
|
|
511
|
+
// was refused hands a prober information it did not have.
|
|
512
|
+
case "unauthorized":
|
|
513
|
+
return {
|
|
514
|
+
status: import_common3.HttpStatus.UNAUTHORIZED,
|
|
515
|
+
code: "UNAUTHORIZED",
|
|
516
|
+
message: error.message
|
|
517
|
+
};
|
|
518
|
+
case "forbidden":
|
|
519
|
+
return {
|
|
520
|
+
status: import_common3.HttpStatus.FORBIDDEN,
|
|
521
|
+
code: "FORBIDDEN",
|
|
522
|
+
message: error.message
|
|
523
|
+
};
|
|
524
|
+
case "model-not-found":
|
|
525
|
+
return {
|
|
526
|
+
status: import_common3.HttpStatus.NOT_FOUND,
|
|
527
|
+
code: "MODEL_NOT_FOUND",
|
|
528
|
+
message: error.message,
|
|
529
|
+
details: {
|
|
530
|
+
model: error.model
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
case "record-not-found":
|
|
534
|
+
return {
|
|
535
|
+
status: import_common3.HttpStatus.NOT_FOUND,
|
|
536
|
+
code: "RECORD_NOT_FOUND",
|
|
537
|
+
message: error.message,
|
|
538
|
+
details: {
|
|
539
|
+
model: error.model,
|
|
540
|
+
id: error.id
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
case "field-not-found":
|
|
544
|
+
return {
|
|
545
|
+
status: import_common3.HttpStatus.BAD_REQUEST,
|
|
546
|
+
code: "FIELD_NOT_FOUND",
|
|
547
|
+
message: error.message,
|
|
548
|
+
details: {
|
|
549
|
+
model: error.model,
|
|
550
|
+
field: error.field
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
// Raised by application code to refuse an input. The message is
|
|
554
|
+
// forwarded, which is what it is for.
|
|
555
|
+
case "validation": {
|
|
556
|
+
const refusal = error;
|
|
557
|
+
return {
|
|
558
|
+
status: import_common3.HttpStatus.BAD_REQUEST,
|
|
559
|
+
code: "VALIDATION_ERROR",
|
|
560
|
+
message: error.message,
|
|
561
|
+
// Only when it named them. An empty list would tell a client the
|
|
562
|
+
// refusal is about no field in particular, which is not the same as
|
|
563
|
+
// not saying - and the interface treats the two differently.
|
|
564
|
+
...refusal.fields.length > 0 ? {
|
|
565
|
+
details: {
|
|
566
|
+
fields: refusal.fields
|
|
567
|
+
}
|
|
568
|
+
} : {}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// The database refused the write for a reason the caller can act on. The
|
|
572
|
+
// message is built from field names rather than taken from the ORM, so it
|
|
573
|
+
// carries no paths or query fragments and is safe to forward.
|
|
574
|
+
case "constraint": {
|
|
575
|
+
const failure2 = error;
|
|
576
|
+
return {
|
|
577
|
+
// A unique clash or a reference still in use is a conflict with data
|
|
578
|
+
// that already exists; a missing required value is a bad request.
|
|
579
|
+
status: failure2.constraint === "required" ? import_common3.HttpStatus.BAD_REQUEST : import_common3.HttpStatus.CONFLICT,
|
|
580
|
+
code: "CONSTRAINT_VIOLATION",
|
|
581
|
+
message: error.message,
|
|
582
|
+
details: {
|
|
583
|
+
constraint: failure2.constraint,
|
|
584
|
+
fields: failure2.fields
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
case "invalid-query":
|
|
589
|
+
return {
|
|
590
|
+
status: import_common3.HttpStatus.BAD_REQUEST,
|
|
591
|
+
code: "INVALID_QUERY",
|
|
592
|
+
message: error.message
|
|
593
|
+
};
|
|
594
|
+
// 'adapter', 'unknown', and any kind added later without a mapping. The
|
|
595
|
+
// default stays generic so a new internal error cannot start leaking.
|
|
596
|
+
default:
|
|
597
|
+
return INTERNAL;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
__name(mapError, "mapError");
|
|
601
|
+
function clientMessage(error) {
|
|
602
|
+
return mapError(error).message;
|
|
603
|
+
}
|
|
604
|
+
__name(clientMessage, "clientMessage");
|
|
605
|
+
var AdminExceptionFilter = class {
|
|
606
|
+
static {
|
|
607
|
+
__name(this, "AdminExceptionFilter");
|
|
608
|
+
}
|
|
609
|
+
logger = new import_common3.Logger("NestAdmin");
|
|
610
|
+
catch(exception, host) {
|
|
611
|
+
if (exception instanceof import_common3.HttpException) {
|
|
612
|
+
throw exception;
|
|
613
|
+
}
|
|
614
|
+
const mapped = mapError(exception);
|
|
615
|
+
if (mapped.status >= import_common3.HttpStatus.INTERNAL_SERVER_ERROR) {
|
|
616
|
+
this.logger.error(isNestAdminError(exception) && exception.kind === "adapter" ? `Adapter failure: ${exception.message}` : "Unhandled error while handling an admin request", exception instanceof Error ? exception.stack : String(exception));
|
|
617
|
+
}
|
|
618
|
+
const body = failure(mapped.code, mapped.message, mapped.details);
|
|
619
|
+
const response = host.switchToHttp().getResponse();
|
|
620
|
+
response.status(mapped.status).json(body);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
AdminExceptionFilter = _ts_decorate2([
|
|
624
|
+
(0, import_common3.Catch)()
|
|
625
|
+
], AdminExceptionFilter);
|
|
626
|
+
|
|
627
|
+
// src/admin/service.ts
|
|
628
|
+
var import_common6 = require("@nestjs/common");
|
|
629
|
+
|
|
630
|
+
// src/auth/built-in.ts
|
|
631
|
+
var import_common4 = require("@nestjs/common");
|
|
632
|
+
|
|
633
|
+
// src/auth/password.ts
|
|
634
|
+
var import_node_crypto = require("crypto");
|
|
635
|
+
var import_node_util = require("util");
|
|
636
|
+
var derive = (0, import_node_util.promisify)(import_node_crypto.scrypt);
|
|
637
|
+
var PARAMS = {
|
|
638
|
+
N: 2 ** 15,
|
|
639
|
+
r: 8,
|
|
640
|
+
p: 1
|
|
641
|
+
};
|
|
642
|
+
var MAXMEM = 128 * PARAMS.N * PARAMS.r * 2;
|
|
643
|
+
var KEY_LENGTH = 64;
|
|
644
|
+
var SALT_BYTES = 16;
|
|
645
|
+
var SEP = String.fromCharCode(36);
|
|
646
|
+
async function hashAdminPassword(password) {
|
|
647
|
+
if (typeof password !== "string" || password.length === 0) {
|
|
648
|
+
throw new Error("A password is required.");
|
|
649
|
+
}
|
|
650
|
+
const salt = (0, import_node_crypto.randomBytes)(SALT_BYTES).toString("hex");
|
|
651
|
+
const key = await derive(password, salt, KEY_LENGTH, {
|
|
652
|
+
...PARAMS,
|
|
653
|
+
maxmem: MAXMEM
|
|
654
|
+
});
|
|
655
|
+
return [
|
|
656
|
+
"scrypt",
|
|
657
|
+
PARAMS.N,
|
|
658
|
+
PARAMS.r,
|
|
659
|
+
PARAMS.p,
|
|
660
|
+
salt,
|
|
661
|
+
key.toString("hex")
|
|
662
|
+
].join(SEP);
|
|
663
|
+
}
|
|
664
|
+
__name(hashAdminPassword, "hashAdminPassword");
|
|
665
|
+
async function verifyAdminPassword(password, stored) {
|
|
666
|
+
const parsed = parse(stored);
|
|
667
|
+
if (!parsed) return false;
|
|
668
|
+
try {
|
|
669
|
+
const key = await derive(password, parsed.salt, parsed.hash.length / 2, {
|
|
670
|
+
N: parsed.N,
|
|
671
|
+
r: parsed.r,
|
|
672
|
+
p: parsed.p,
|
|
673
|
+
maxmem: 128 * parsed.N * parsed.r * 2
|
|
674
|
+
});
|
|
675
|
+
const expected = Buffer.from(parsed.hash, "hex");
|
|
676
|
+
return key.length === expected.length && (0, import_node_crypto.timingSafeEqual)(key, expected);
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
__name(verifyAdminPassword, "verifyAdminPassword");
|
|
682
|
+
function parse(stored) {
|
|
683
|
+
if (typeof stored !== "string") return void 0;
|
|
684
|
+
const [scheme, n, r, p, salt, hash] = stored.split(SEP);
|
|
685
|
+
if (scheme !== "scrypt" || salt === void 0 || hash === void 0) return void 0;
|
|
686
|
+
const N = Number(n);
|
|
687
|
+
const rounds = Number(r);
|
|
688
|
+
const parallel = Number(p);
|
|
689
|
+
const usable = Number.isInteger(N) && N >= 2 ** 12 && N <= 2 ** 20 && (N & N - 1) === 0 && Number.isInteger(rounds) && rounds > 0 && rounds <= 32 && Number.isInteger(parallel) && parallel > 0 && parallel <= 16 && /^[0-9a-f]+$/i.test(salt) && /^[0-9a-f]+$/i.test(hash) && hash.length % 2 === 0;
|
|
690
|
+
return usable ? {
|
|
691
|
+
N,
|
|
692
|
+
r: rounds,
|
|
693
|
+
p: parallel,
|
|
694
|
+
salt,
|
|
695
|
+
hash
|
|
696
|
+
} : void 0;
|
|
697
|
+
}
|
|
698
|
+
__name(parse, "parse");
|
|
699
|
+
var NO_SUCH_ACCOUNT = hashAdminPassword((0, import_node_crypto.randomBytes)(32).toString("hex"));
|
|
700
|
+
|
|
701
|
+
// src/auth/session.ts
|
|
702
|
+
var import_node_crypto2 = require("crypto");
|
|
703
|
+
var VERSION = "v1";
|
|
704
|
+
var encode = /* @__PURE__ */ __name((value) => Buffer.from(value).toString("base64url"), "encode");
|
|
705
|
+
function signSession(accountId, secret, lifetime) {
|
|
706
|
+
const payload = {
|
|
707
|
+
sub: accountId,
|
|
708
|
+
exp: Math.floor(Date.now() / 1e3) + lifetime
|
|
709
|
+
};
|
|
710
|
+
const body = `${VERSION}.${encode(JSON.stringify(payload))}`;
|
|
711
|
+
return `${body}.${sign(body, secret)}`;
|
|
712
|
+
}
|
|
713
|
+
__name(signSession, "signSession");
|
|
714
|
+
function readSession(token, secret) {
|
|
715
|
+
if (typeof token !== "string") return void 0;
|
|
716
|
+
const cut = token.lastIndexOf(".");
|
|
717
|
+
if (cut < 1) return void 0;
|
|
718
|
+
const body = token.slice(0, cut);
|
|
719
|
+
const presented = token.slice(cut + 1);
|
|
720
|
+
if (!body.startsWith(`${VERSION}.`)) return void 0;
|
|
721
|
+
if (!matches(presented, sign(body, secret))) return void 0;
|
|
722
|
+
try {
|
|
723
|
+
const payload = JSON.parse(Buffer.from(body.slice(VERSION.length + 1), "base64url").toString("utf8"));
|
|
724
|
+
if (typeof payload.sub !== "string" || payload.sub === "") return void 0;
|
|
725
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now()) return void 0;
|
|
726
|
+
return payload.sub;
|
|
727
|
+
} catch {
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
__name(readSession, "readSession");
|
|
732
|
+
function shouldRenew(token, secret, lifetime) {
|
|
733
|
+
const cut = token.lastIndexOf(".");
|
|
734
|
+
if (cut < 1 || !matches(token.slice(cut + 1), sign(token.slice(0, cut), secret))) return false;
|
|
735
|
+
try {
|
|
736
|
+
const payload = JSON.parse(Buffer.from(token.slice(VERSION.length + 1, cut), "base64url").toString("utf8"));
|
|
737
|
+
const remaining = payload.exp - Math.floor(Date.now() / 1e3);
|
|
738
|
+
return remaining < lifetime / 2;
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
__name(shouldRenew, "shouldRenew");
|
|
744
|
+
function sign(body, secret) {
|
|
745
|
+
return (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("base64url");
|
|
746
|
+
}
|
|
747
|
+
__name(sign, "sign");
|
|
748
|
+
function matches(presented, expected) {
|
|
749
|
+
const a = Buffer.from(presented);
|
|
750
|
+
const b = Buffer.from(expected);
|
|
751
|
+
return a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
752
|
+
}
|
|
753
|
+
__name(matches, "matches");
|
|
754
|
+
var MIN_SECRET_LENGTH = 32;
|
|
755
|
+
function generateSessionSecret() {
|
|
756
|
+
return (0, import_node_crypto2.randomBytes)(32).toString("base64url");
|
|
757
|
+
}
|
|
758
|
+
__name(generateSessionSecret, "generateSessionSecret");
|
|
759
|
+
|
|
760
|
+
// src/auth/built-in.ts
|
|
761
|
+
var logger = new import_common4.Logger("NestAdmin");
|
|
762
|
+
var DEFAULT_MAX_AGE = 12 * 60 * 60;
|
|
763
|
+
var RUNTIME = /* @__PURE__ */ Symbol.for("nest-admin.built-in-auth");
|
|
764
|
+
function builtInRuntimeOf(auth) {
|
|
765
|
+
return typeof auth === "object" && auth !== null ? auth[RUNTIME] ?? void 0 : void 0;
|
|
766
|
+
}
|
|
767
|
+
__name(builtInRuntimeOf, "builtInRuntimeOf");
|
|
768
|
+
function adminAccountOf(context) {
|
|
769
|
+
const request = context.switchToHttp().getRequest();
|
|
770
|
+
return request?.adminAccount;
|
|
771
|
+
}
|
|
772
|
+
__name(adminAccountOf, "adminAccountOf");
|
|
773
|
+
function builtInAuth(options) {
|
|
774
|
+
const secret = options.session?.secret;
|
|
775
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
776
|
+
throw new Error(`builtInAuth() requires \`session.secret\` of at least ${MIN_SECRET_LENGTH} characters. A short secret can be guessed, and a guessed one mints a session for any account. Read it from the environment rather than writing it here.`);
|
|
777
|
+
}
|
|
778
|
+
if (!options.store || typeof options.store.findByEmail !== "function") {
|
|
779
|
+
throw new Error("builtInAuth() requires a `store`. Use `prismaAccountStore({ client })` from `@nest-admin/nestjs/prisma`, or supply your own AdminAccountStore.");
|
|
780
|
+
}
|
|
781
|
+
const maxAge = options.session.maxAge ?? DEFAULT_MAX_AGE;
|
|
782
|
+
const cookieName = options.session.cookieName ?? "nest_admin_session";
|
|
783
|
+
const attempts = new Attempts(options.maxAttempts ?? 10, options.lockoutSeconds ?? 15 * 60);
|
|
784
|
+
const runtime = {
|
|
785
|
+
store: options.store,
|
|
786
|
+
secret,
|
|
787
|
+
maxAge,
|
|
788
|
+
cookieName,
|
|
789
|
+
secure: options.session.secure,
|
|
790
|
+
async signIn(email, password, from) {
|
|
791
|
+
if (typeof email !== "string" || typeof password !== "string") return void 0;
|
|
792
|
+
if (attempts.lockedOut(from)) return void 0;
|
|
793
|
+
const account = await options.store.findByEmail(email.trim().toLowerCase());
|
|
794
|
+
const stored = account?.passwordHash ?? await NO_SUCH_ACCOUNT;
|
|
795
|
+
const correct = await verifyAdminPassword(password, stored);
|
|
796
|
+
if (!correct || !account || account.disabled === true) {
|
|
797
|
+
attempts.failed(from);
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
attempts.succeeded(from);
|
|
801
|
+
void options.store.recordLogin?.(account.id).catch((cause) => {
|
|
802
|
+
logger.warn(`Could not record a login: ${String(cause)}`);
|
|
803
|
+
});
|
|
804
|
+
return account;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
const auth = {
|
|
808
|
+
async authorize(context) {
|
|
809
|
+
const request = context.switchToHttp().getRequest();
|
|
810
|
+
const token = cookieFrom(request?.headers?.cookie, cookieName);
|
|
811
|
+
const id = token === void 0 ? void 0 : readSession(token, secret);
|
|
812
|
+
if (id === void 0) throw new UnauthorizedError("Sign in to continue.");
|
|
813
|
+
const account = await options.store.findById(id);
|
|
814
|
+
if (!account || account.disabled === true) {
|
|
815
|
+
throw new UnauthorizedError("Sign in to continue.");
|
|
816
|
+
}
|
|
817
|
+
request.adminAccount = summarise(account);
|
|
818
|
+
if (token !== void 0 && shouldRenew(token, secret, maxAge)) {
|
|
819
|
+
setSessionCookie(context.switchToHttp().getResponse(), signSession(account.id, secret, maxAge), runtime, request);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
Object.defineProperty(auth, RUNTIME, {
|
|
824
|
+
value: runtime,
|
|
825
|
+
enumerable: false
|
|
826
|
+
});
|
|
827
|
+
return auth;
|
|
828
|
+
}
|
|
829
|
+
__name(builtInAuth, "builtInAuth");
|
|
830
|
+
function cookieFrom(header2, name) {
|
|
831
|
+
if (typeof header2 !== "string") return void 0;
|
|
832
|
+
for (const part of header2.split(";")) {
|
|
833
|
+
const eq = part.indexOf("=");
|
|
834
|
+
if (eq < 0) continue;
|
|
835
|
+
if (part.slice(0, eq).trim() !== name) continue;
|
|
836
|
+
const value = part.slice(eq + 1).trim();
|
|
837
|
+
try {
|
|
838
|
+
return decodeURIComponent(value);
|
|
839
|
+
} catch {
|
|
840
|
+
return value;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return void 0;
|
|
844
|
+
}
|
|
845
|
+
__name(cookieFrom, "cookieFrom");
|
|
846
|
+
function setSessionCookie(response, token, runtime, request) {
|
|
847
|
+
writeCookie(response, [
|
|
848
|
+
`${runtime.cookieName}=${token}`,
|
|
849
|
+
"Path=/",
|
|
850
|
+
"HttpOnly",
|
|
851
|
+
"SameSite=Lax",
|
|
852
|
+
`Max-Age=${runtime.maxAge}`,
|
|
853
|
+
...isSecure(runtime.secure, request) ? [
|
|
854
|
+
"Secure"
|
|
855
|
+
] : []
|
|
856
|
+
].join("; "));
|
|
857
|
+
}
|
|
858
|
+
__name(setSessionCookie, "setSessionCookie");
|
|
859
|
+
function clearSessionCookie(response, runtime, request) {
|
|
860
|
+
writeCookie(response, [
|
|
861
|
+
`${runtime.cookieName}=`,
|
|
862
|
+
"Path=/",
|
|
863
|
+
"HttpOnly",
|
|
864
|
+
"SameSite=Lax",
|
|
865
|
+
"Max-Age=0",
|
|
866
|
+
...isSecure(runtime.secure, request) ? [
|
|
867
|
+
"Secure"
|
|
868
|
+
] : []
|
|
869
|
+
].join("; "));
|
|
870
|
+
}
|
|
871
|
+
__name(clearSessionCookie, "clearSessionCookie");
|
|
872
|
+
function isSecure(configured, request) {
|
|
873
|
+
if (configured !== void 0) return configured;
|
|
874
|
+
const host = String(request?.headers?.["host"] ?? "");
|
|
875
|
+
const local = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host);
|
|
876
|
+
return !local;
|
|
877
|
+
}
|
|
878
|
+
__name(isSecure, "isSecure");
|
|
879
|
+
function writeCookie(response, value) {
|
|
880
|
+
const target = response;
|
|
881
|
+
if (typeof target?.setHeader === "function") return target.setHeader("Set-Cookie", value);
|
|
882
|
+
if (typeof target?.header === "function") target.header("Set-Cookie", value);
|
|
883
|
+
}
|
|
884
|
+
__name(writeCookie, "writeCookie");
|
|
885
|
+
function attemptKey(request, email) {
|
|
886
|
+
const address = (typeof request?.ip === "string" ? request.ip : void 0) ?? request?.socket?.remoteAddress ?? "unknown";
|
|
887
|
+
return `${address}|${typeof email === "string" ? email.trim().toLowerCase() : ""}`;
|
|
888
|
+
}
|
|
889
|
+
__name(attemptKey, "attemptKey");
|
|
890
|
+
var Attempts = class Attempts2 {
|
|
891
|
+
static {
|
|
892
|
+
__name(this, "Attempts");
|
|
893
|
+
}
|
|
894
|
+
max;
|
|
895
|
+
seconds;
|
|
896
|
+
#failures = /* @__PURE__ */ new Map();
|
|
897
|
+
constructor(max, seconds) {
|
|
898
|
+
this.max = max;
|
|
899
|
+
this.seconds = seconds;
|
|
900
|
+
}
|
|
901
|
+
lockedOut(key) {
|
|
902
|
+
const entry = this.#failures.get(key);
|
|
903
|
+
if (!entry) return false;
|
|
904
|
+
if (entry.until === 0) return false;
|
|
905
|
+
if (entry.until > Date.now()) return true;
|
|
906
|
+
this.#failures.delete(key);
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
failed(key) {
|
|
910
|
+
const now = Date.now();
|
|
911
|
+
const existing = this.#failures.get(key);
|
|
912
|
+
const entry = existing && now - existing.since < this.seconds * 1e3 ? existing : {
|
|
913
|
+
count: 0,
|
|
914
|
+
since: now,
|
|
915
|
+
until: 0
|
|
916
|
+
};
|
|
917
|
+
entry.count += 1;
|
|
918
|
+
if (entry.count >= this.max) entry.until = now + this.seconds * 1e3;
|
|
919
|
+
this.#failures.set(key, entry);
|
|
920
|
+
if (this.#failures.size > 1e4) this.#prune();
|
|
921
|
+
}
|
|
922
|
+
succeeded(key) {
|
|
923
|
+
this.#failures.delete(key);
|
|
924
|
+
}
|
|
925
|
+
#prune() {
|
|
926
|
+
const now = Date.now();
|
|
927
|
+
for (const [key, entry] of this.#failures) {
|
|
928
|
+
const locked = entry.until > now;
|
|
929
|
+
const recent = now - entry.since < this.seconds * 1e3;
|
|
930
|
+
if (!locked && !recent) this.#failures.delete(key);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// src/dashboard/service.ts
|
|
936
|
+
var import_common5 = require("@nestjs/common");
|
|
937
|
+
|
|
938
|
+
// src/http/query-parser.ts
|
|
939
|
+
var FILTER_OPERATORS = [
|
|
940
|
+
"eq",
|
|
941
|
+
"ne",
|
|
942
|
+
"contains",
|
|
943
|
+
"startsWith",
|
|
944
|
+
"endsWith",
|
|
945
|
+
"gt",
|
|
946
|
+
"gte",
|
|
947
|
+
"lt",
|
|
948
|
+
"lte",
|
|
949
|
+
"in"
|
|
950
|
+
];
|
|
951
|
+
var SORT_DIRECTIONS = /* @__PURE__ */ new Set([
|
|
952
|
+
"asc",
|
|
953
|
+
"desc"
|
|
954
|
+
]);
|
|
955
|
+
function rejectStructuredValue(name, value) {
|
|
956
|
+
if (value === void 0 || value === null) return;
|
|
957
|
+
if (typeof value === "string") return;
|
|
958
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) return;
|
|
959
|
+
throw new InvalidQueryError(`"${name}" must be a plain value, not a nested structure. This API uses colon syntax - for example "?filter=age:gte:18" and "?sort=email:asc", not "?filter[age][gte]=18".`);
|
|
960
|
+
}
|
|
961
|
+
__name(rejectStructuredValue, "rejectStructuredValue");
|
|
962
|
+
var KNOWN_PARAMETERS = /* @__PURE__ */ new Set([
|
|
963
|
+
"page",
|
|
964
|
+
"perPage",
|
|
965
|
+
"search",
|
|
966
|
+
"sort",
|
|
967
|
+
"filter"
|
|
968
|
+
]);
|
|
969
|
+
function rejectUnknownParameters(raw) {
|
|
970
|
+
const unknown = Object.keys(raw).filter((key) => !KNOWN_PARAMETERS.has(key));
|
|
971
|
+
if (unknown.length === 0) return;
|
|
972
|
+
const looksBracketed = unknown.some((key) => key.includes("["));
|
|
973
|
+
const hint = looksBracketed ? ' This API uses colon syntax: "?filter=age:gte:18", not "?filter[age][gte]=18".' : "";
|
|
974
|
+
throw new InvalidQueryError(`Unknown query parameter${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Supported: ${[
|
|
975
|
+
...KNOWN_PARAMETERS
|
|
976
|
+
].join(", ")}.${hint}`);
|
|
977
|
+
}
|
|
978
|
+
__name(rejectUnknownParameters, "rejectUnknownParameters");
|
|
979
|
+
function toStringList(name, value) {
|
|
980
|
+
rejectStructuredValue(name, value);
|
|
981
|
+
if (value === void 0 || value === null) return [];
|
|
982
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
|
|
983
|
+
return typeof value === "string" ? [
|
|
984
|
+
value
|
|
985
|
+
] : [];
|
|
986
|
+
}
|
|
987
|
+
__name(toStringList, "toStringList");
|
|
988
|
+
function toSingleString(name, value) {
|
|
989
|
+
rejectStructuredValue(name, value);
|
|
990
|
+
if (typeof value === "string") return value;
|
|
991
|
+
if (Array.isArray(value)) {
|
|
992
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
993
|
+
return strings.at(-1);
|
|
994
|
+
}
|
|
995
|
+
return void 0;
|
|
996
|
+
}
|
|
997
|
+
__name(toSingleString, "toSingleString");
|
|
998
|
+
function parsePositiveInteger(raw, name) {
|
|
999
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
1000
|
+
if (!/^\d+$/.test(raw)) {
|
|
1001
|
+
throw new InvalidQueryError(`"${name}" must be a positive integer, received ${JSON.stringify(raw)}.`);
|
|
1002
|
+
}
|
|
1003
|
+
const parsed = Number(raw);
|
|
1004
|
+
if (parsed < 1) {
|
|
1005
|
+
throw new InvalidQueryError(`"${name}" must be >= 1, received ${JSON.stringify(raw)}.`);
|
|
1006
|
+
}
|
|
1007
|
+
return parsed;
|
|
1008
|
+
}
|
|
1009
|
+
__name(parsePositiveInteger, "parsePositiveInteger");
|
|
1010
|
+
function parseSort(raw) {
|
|
1011
|
+
const entries = toStringList("sort", raw).filter((entry) => entry.trim() !== "");
|
|
1012
|
+
if (entries.length === 0) return void 0;
|
|
1013
|
+
return entries.map((entry) => {
|
|
1014
|
+
const separator = entry.lastIndexOf(":");
|
|
1015
|
+
if (separator <= 0 || separator === entry.length - 1) {
|
|
1016
|
+
throw new InvalidQueryError(`Invalid sort "${entry}". Expected "field:asc" or "field:desc".`);
|
|
1017
|
+
}
|
|
1018
|
+
const field = entry.slice(0, separator);
|
|
1019
|
+
const direction = entry.slice(separator + 1);
|
|
1020
|
+
if (!SORT_DIRECTIONS.has(direction)) {
|
|
1021
|
+
throw new InvalidQueryError(`Invalid sort direction "${direction}" in "${entry}". Expected "asc" or "desc".`);
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
field,
|
|
1025
|
+
direction
|
|
1026
|
+
};
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
__name(parseSort, "parseSort");
|
|
1030
|
+
function coerceScalar(raw, field, context) {
|
|
1031
|
+
if (!field) return raw;
|
|
1032
|
+
switch (field.kind) {
|
|
1033
|
+
case "number": {
|
|
1034
|
+
const parsed = Number(raw);
|
|
1035
|
+
if (raw.trim() === "" || !Number.isFinite(parsed)) {
|
|
1036
|
+
throw new InvalidQueryError(`${context}: "${raw}" is not a valid number.`);
|
|
1037
|
+
}
|
|
1038
|
+
return parsed;
|
|
1039
|
+
}
|
|
1040
|
+
case "boolean": {
|
|
1041
|
+
if (raw === "true") return true;
|
|
1042
|
+
if (raw === "false") return false;
|
|
1043
|
+
throw new InvalidQueryError(`${context}: "${raw}" is not a valid boolean (use true or false).`);
|
|
1044
|
+
}
|
|
1045
|
+
case "datetime": {
|
|
1046
|
+
const parsed = new Date(raw);
|
|
1047
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1048
|
+
throw new InvalidQueryError(`${context}: "${raw}" is not a valid date.`);
|
|
1049
|
+
}
|
|
1050
|
+
return parsed;
|
|
1051
|
+
}
|
|
1052
|
+
default:
|
|
1053
|
+
return raw;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
__name(coerceScalar, "coerceScalar");
|
|
1057
|
+
function coerceList(raw, field, context) {
|
|
1058
|
+
if (raw === "") return [];
|
|
1059
|
+
return raw.split(",").map((part) => coerceScalar(part, field, context));
|
|
1060
|
+
}
|
|
1061
|
+
__name(coerceList, "coerceList");
|
|
1062
|
+
function parseFilters(raw, model) {
|
|
1063
|
+
const entries = toStringList("filter", raw).filter((entry) => entry.trim() !== "");
|
|
1064
|
+
if (entries.length === 0) return void 0;
|
|
1065
|
+
return entries.map((entry) => {
|
|
1066
|
+
const firstSeparator = entry.indexOf(":");
|
|
1067
|
+
const secondSeparator = firstSeparator === -1 ? -1 : entry.indexOf(":", firstSeparator + 1);
|
|
1068
|
+
if (firstSeparator <= 0 || secondSeparator === -1) {
|
|
1069
|
+
throw new InvalidQueryError(`Invalid filter "${entry}". Expected "field:operator:value", for example "email:contains:example.com".`);
|
|
1070
|
+
}
|
|
1071
|
+
const fieldName = entry.slice(0, firstSeparator);
|
|
1072
|
+
const operator = entry.slice(firstSeparator + 1, secondSeparator);
|
|
1073
|
+
const rawValue = entry.slice(secondSeparator + 1);
|
|
1074
|
+
if (!FILTER_OPERATORS.includes(operator)) {
|
|
1075
|
+
throw new InvalidQueryError(`Unknown filter operator "${operator}" in "${entry}". Supported operators: ${FILTER_OPERATORS.join(", ")}.`);
|
|
1076
|
+
}
|
|
1077
|
+
const field = model.fields.find((candidate) => candidate.name === fieldName);
|
|
1078
|
+
const context = `Filter "${entry}"`;
|
|
1079
|
+
const value = operator === "in" ? coerceList(rawValue, field, context) : coerceScalar(rawValue, field, context);
|
|
1080
|
+
return {
|
|
1081
|
+
field: fieldName,
|
|
1082
|
+
operator,
|
|
1083
|
+
value
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
__name(parseFilters, "parseFilters");
|
|
1088
|
+
function parseFilterExpression(entry, model) {
|
|
1089
|
+
const rules = parseFilters(entry, model);
|
|
1090
|
+
const rule = rules?.[0];
|
|
1091
|
+
if (rule === void 0) {
|
|
1092
|
+
throw new InvalidQueryError(`Expected a filter of the form "field:operator:value", got "${entry}".`);
|
|
1093
|
+
}
|
|
1094
|
+
return rule;
|
|
1095
|
+
}
|
|
1096
|
+
__name(parseFilterExpression, "parseFilterExpression");
|
|
1097
|
+
function parseListQuery(raw, model) {
|
|
1098
|
+
rejectUnknownParameters(raw);
|
|
1099
|
+
const page = parsePositiveInteger(toSingleString("page", raw["page"]), "page");
|
|
1100
|
+
const perPage = parsePositiveInteger(toSingleString("perPage", raw["perPage"]), "perPage");
|
|
1101
|
+
const sort = parseSort(raw["sort"]);
|
|
1102
|
+
const filters = parseFilters(raw["filter"], model);
|
|
1103
|
+
const search = toSingleString("search", raw["search"]);
|
|
1104
|
+
return {
|
|
1105
|
+
...page !== void 0 ? {
|
|
1106
|
+
page
|
|
1107
|
+
} : {},
|
|
1108
|
+
...perPage !== void 0 ? {
|
|
1109
|
+
perPage
|
|
1110
|
+
} : {},
|
|
1111
|
+
...sort ? {
|
|
1112
|
+
sort
|
|
1113
|
+
} : {},
|
|
1114
|
+
...filters ? {
|
|
1115
|
+
filters
|
|
1116
|
+
} : {},
|
|
1117
|
+
...search !== void 0 && search !== "" ? {
|
|
1118
|
+
search
|
|
1119
|
+
} : {}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
__name(parseListQuery, "parseListQuery");
|
|
1123
|
+
|
|
1124
|
+
// src/dashboard/contract.ts
|
|
1125
|
+
function modelOf(widget) {
|
|
1126
|
+
return widget.kind === "stat" ? void 0 : widget.model;
|
|
1127
|
+
}
|
|
1128
|
+
__name(modelOf, "modelOf");
|
|
1129
|
+
function defaultSpan(widget) {
|
|
1130
|
+
switch (widget.kind) {
|
|
1131
|
+
// A number is small; a chart needs room to be read; a list is a column of
|
|
1132
|
+
// rows and looks thin at a quarter width.
|
|
1133
|
+
case "chart":
|
|
1134
|
+
return 2;
|
|
1135
|
+
case "list":
|
|
1136
|
+
return 2;
|
|
1137
|
+
default:
|
|
1138
|
+
return 1;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
__name(defaultSpan, "defaultSpan");
|
|
1142
|
+
|
|
1143
|
+
// src/dashboard/service.ts
|
|
1144
|
+
var logger2 = new import_common5.Logger("NestAdmin");
|
|
1145
|
+
var MAX_BUCKETS = 90;
|
|
1146
|
+
var DAY = 864e5;
|
|
1147
|
+
async function buildDashboard(input) {
|
|
1148
|
+
const declared = input.declared;
|
|
1149
|
+
const generated = declared === void 0 || declared.length === 0;
|
|
1150
|
+
const widgets = generated ? generateFrom(input.models, input.labels ?? {}) : declared;
|
|
1151
|
+
const visible = new Set(input.models.map((model) => model.name));
|
|
1152
|
+
const resolved = await Promise.all(widgets.filter((widget) => {
|
|
1153
|
+
const model = modelOf(widget);
|
|
1154
|
+
return model === void 0 || visible.has(model);
|
|
1155
|
+
}).map((widget, index) => resolve(widget, index, input)));
|
|
1156
|
+
return {
|
|
1157
|
+
widgets: resolved,
|
|
1158
|
+
generated
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
__name(buildDashboard, "buildDashboard");
|
|
1162
|
+
function generateFrom(models, labels) {
|
|
1163
|
+
const labelOf = /* @__PURE__ */ __name((model) => labels[model.name] ?? model.name, "labelOf");
|
|
1164
|
+
const widgets = models.map((model) => ({
|
|
1165
|
+
kind: "count",
|
|
1166
|
+
title: labelOf(model),
|
|
1167
|
+
model: model.name
|
|
1168
|
+
}));
|
|
1169
|
+
const dated = models.filter((model) => createdFieldFor(model) !== void 0);
|
|
1170
|
+
const first = dated[0];
|
|
1171
|
+
if (first) {
|
|
1172
|
+
widgets.push({
|
|
1173
|
+
kind: "chart",
|
|
1174
|
+
title: `New ${labelOf(first)}`,
|
|
1175
|
+
description: "Over the last 30 days.",
|
|
1176
|
+
model: first.name,
|
|
1177
|
+
span: 2
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
for (const model of dated.slice(0, 2)) {
|
|
1181
|
+
widgets.push({
|
|
1182
|
+
kind: "list",
|
|
1183
|
+
title: `Recent ${labelOf(model)}`,
|
|
1184
|
+
model: model.name,
|
|
1185
|
+
span: 2
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return widgets;
|
|
1189
|
+
}
|
|
1190
|
+
__name(generateFrom, "generateFrom");
|
|
1191
|
+
async function resolve(widget, index, input) {
|
|
1192
|
+
const base = {
|
|
1193
|
+
// Stable within one document, and only used as a React key: a title is not
|
|
1194
|
+
// unique and a position changes when a widget above it is dropped.
|
|
1195
|
+
id: `${widget.kind}-${index}`,
|
|
1196
|
+
kind: widget.kind,
|
|
1197
|
+
title: widget.title,
|
|
1198
|
+
span: widget.span ?? defaultSpan(widget),
|
|
1199
|
+
...widget.description !== void 0 ? {
|
|
1200
|
+
description: widget.description
|
|
1201
|
+
} : {},
|
|
1202
|
+
...widget.kind !== "stat" ? {
|
|
1203
|
+
model: widget.model
|
|
1204
|
+
} : {},
|
|
1205
|
+
...widget.kind !== "stat" && widget.filter !== void 0 ? {
|
|
1206
|
+
filter: widget.filter
|
|
1207
|
+
} : {}
|
|
1208
|
+
};
|
|
1209
|
+
try {
|
|
1210
|
+
return {
|
|
1211
|
+
...base,
|
|
1212
|
+
data: await dataFor(widget, input)
|
|
1213
|
+
};
|
|
1214
|
+
} catch (cause) {
|
|
1215
|
+
logger2.warn(`Dashboard widget "${widget.title}" failed: ${String(cause)}`);
|
|
1216
|
+
return {
|
|
1217
|
+
...base,
|
|
1218
|
+
failed: true
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
__name(resolve, "resolve");
|
|
1223
|
+
async function dataFor(widget, input) {
|
|
1224
|
+
switch (widget.kind) {
|
|
1225
|
+
case "stat":
|
|
1226
|
+
return widget.load({
|
|
1227
|
+
context: input.context
|
|
1228
|
+
});
|
|
1229
|
+
case "count":
|
|
1230
|
+
return countOf(widget, input);
|
|
1231
|
+
case "list":
|
|
1232
|
+
return listOf(widget, input);
|
|
1233
|
+
case "chart":
|
|
1234
|
+
return chartOf(widget, input);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
__name(dataFor, "dataFor");
|
|
1238
|
+
async function countOf(widget, input) {
|
|
1239
|
+
const model = modelFor(widget.model, input);
|
|
1240
|
+
const declared = filtersFor(widget.filter, model);
|
|
1241
|
+
const page = await input.adapter.list(widget.model, {
|
|
1242
|
+
perPage: 1,
|
|
1243
|
+
...declared.length > 0 ? {
|
|
1244
|
+
filters: declared
|
|
1245
|
+
} : {}
|
|
1246
|
+
});
|
|
1247
|
+
if (widget.compareDays === void 0) return {
|
|
1248
|
+
value: page.total
|
|
1249
|
+
};
|
|
1250
|
+
const created = createdFieldFor(model);
|
|
1251
|
+
if (created === void 0) return {
|
|
1252
|
+
value: page.total
|
|
1253
|
+
};
|
|
1254
|
+
const since = new Date(Date.now() - widget.compareDays * DAY).toISOString();
|
|
1255
|
+
const recent = await input.adapter.list(widget.model, {
|
|
1256
|
+
perPage: 1,
|
|
1257
|
+
filters: [
|
|
1258
|
+
...declared,
|
|
1259
|
+
{
|
|
1260
|
+
field: created,
|
|
1261
|
+
operator: "gte",
|
|
1262
|
+
value: since
|
|
1263
|
+
}
|
|
1264
|
+
]
|
|
1265
|
+
});
|
|
1266
|
+
const before = page.total - recent.total;
|
|
1267
|
+
return {
|
|
1268
|
+
value: page.total,
|
|
1269
|
+
// Everything is new when there was nothing before, and dividing by zero to
|
|
1270
|
+
// say so would produce Infinity on a brand-new install.
|
|
1271
|
+
...before > 0 ? {
|
|
1272
|
+
delta: Math.round(recent.total / before * 100)
|
|
1273
|
+
} : {},
|
|
1274
|
+
hint: `${recent.total} in the last ${widget.compareDays} days`
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
__name(countOf, "countOf");
|
|
1278
|
+
async function listOf(widget, input) {
|
|
1279
|
+
const model = modelFor(widget.model, input);
|
|
1280
|
+
const created = createdFieldFor(model);
|
|
1281
|
+
const label = displayFieldFor(model);
|
|
1282
|
+
const key = model.primaryKey[0] ?? "id";
|
|
1283
|
+
const declared = filtersFor(widget.filter, model);
|
|
1284
|
+
const page = await input.adapter.list(widget.model, {
|
|
1285
|
+
perPage: Math.min(widget.limit ?? 5, 10),
|
|
1286
|
+
// Newest first where the model says which those are; otherwise whatever
|
|
1287
|
+
// order the adapter returns, which is better than refusing to show a list.
|
|
1288
|
+
...created ? {
|
|
1289
|
+
sort: [
|
|
1290
|
+
{
|
|
1291
|
+
field: created,
|
|
1292
|
+
direction: "desc"
|
|
1293
|
+
}
|
|
1294
|
+
]
|
|
1295
|
+
} : {},
|
|
1296
|
+
...declared.length > 0 ? {
|
|
1297
|
+
filters: declared
|
|
1298
|
+
} : {}
|
|
1299
|
+
});
|
|
1300
|
+
return {
|
|
1301
|
+
total: page.total,
|
|
1302
|
+
records: page.data.map((record) => ({
|
|
1303
|
+
id: String(record[key] ?? ""),
|
|
1304
|
+
label: readable(record[label]) ?? String(record[key] ?? "")
|
|
1305
|
+
}))
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
__name(listOf, "listOf");
|
|
1309
|
+
async function chartOf(widget, input) {
|
|
1310
|
+
const model = modelFor(widget.model, input);
|
|
1311
|
+
const created = createdFieldFor(model);
|
|
1312
|
+
if (created === void 0) {
|
|
1313
|
+
throw new Error(`${widget.model} has no creation timestamp, so it cannot be charted.`);
|
|
1314
|
+
}
|
|
1315
|
+
const bucket = widget.bucket ?? "day";
|
|
1316
|
+
const count = Math.min(widget.buckets ?? 30, MAX_BUCKETS);
|
|
1317
|
+
const size = bucket === "day" ? DAY : bucket === "week" ? 7 * DAY : 30 * DAY;
|
|
1318
|
+
const declared = filtersFor(widget.filter, model);
|
|
1319
|
+
const now = Date.now();
|
|
1320
|
+
const starts = Array.from({
|
|
1321
|
+
length: count
|
|
1322
|
+
}, (_, index) => now - (count - index) * size);
|
|
1323
|
+
const points = await Promise.all(starts.map(async (start) => {
|
|
1324
|
+
const page = await input.adapter.list(widget.model, {
|
|
1325
|
+
perPage: 1,
|
|
1326
|
+
filters: [
|
|
1327
|
+
...declared,
|
|
1328
|
+
{
|
|
1329
|
+
field: created,
|
|
1330
|
+
operator: "gte",
|
|
1331
|
+
value: new Date(start).toISOString()
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
field: created,
|
|
1335
|
+
operator: "lt",
|
|
1336
|
+
value: new Date(start + size).toISOString()
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1339
|
+
});
|
|
1340
|
+
return {
|
|
1341
|
+
at: new Date(start).toISOString(),
|
|
1342
|
+
value: page.total
|
|
1343
|
+
};
|
|
1344
|
+
}));
|
|
1345
|
+
return {
|
|
1346
|
+
points,
|
|
1347
|
+
total: points.reduce((sum, point) => sum + point.value, 0)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
__name(chartOf, "chartOf");
|
|
1351
|
+
function modelFor(name, input) {
|
|
1352
|
+
const model = input.models.find((candidate) => candidate.name === name);
|
|
1353
|
+
if (model === void 0) throw new Error(`${name} is not an exposed model.`);
|
|
1354
|
+
return model;
|
|
1355
|
+
}
|
|
1356
|
+
__name(modelFor, "modelFor");
|
|
1357
|
+
function filtersFor(filter, model) {
|
|
1358
|
+
return filter === void 0 ? [] : [
|
|
1359
|
+
parseFilterExpression(filter, model)
|
|
1360
|
+
];
|
|
1361
|
+
}
|
|
1362
|
+
__name(filtersFor, "filtersFor");
|
|
1363
|
+
function readable(value) {
|
|
1364
|
+
if (typeof value === "string" && value !== "") return value;
|
|
1365
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1366
|
+
return void 0;
|
|
1367
|
+
}
|
|
1368
|
+
__name(readable, "readable");
|
|
1369
|
+
|
|
1370
|
+
// src/admin/metadata.dto.ts
|
|
1371
|
+
var ALL_PERMITTED = {
|
|
1372
|
+
list: true,
|
|
1373
|
+
read: true,
|
|
1374
|
+
create: true,
|
|
1375
|
+
update: true,
|
|
1376
|
+
delete: true
|
|
1377
|
+
};
|
|
1378
|
+
function targetForeignKeyOf(field, models) {
|
|
1379
|
+
if (relationShape(field, models) !== "one-to-many") return void 0;
|
|
1380
|
+
return inverseRelationField(field, models)?.relation?.from;
|
|
1381
|
+
}
|
|
1382
|
+
__name(targetForeignKeyOf, "targetForeignKeyOf");
|
|
1383
|
+
function byOrder(items, orderOf) {
|
|
1384
|
+
return [
|
|
1385
|
+
...items
|
|
1386
|
+
].map((item, index) => ({
|
|
1387
|
+
item,
|
|
1388
|
+
index,
|
|
1389
|
+
order: orderOf(item)
|
|
1390
|
+
})).sort((a, b) => {
|
|
1391
|
+
if (a.order === b.order) return a.index - b.index;
|
|
1392
|
+
if (a.order === void 0) return 1;
|
|
1393
|
+
if (b.order === void 0) return -1;
|
|
1394
|
+
return a.order - b.order;
|
|
1395
|
+
}).map((entry) => entry.item);
|
|
1396
|
+
}
|
|
1397
|
+
__name(byOrder, "byOrder");
|
|
1398
|
+
function toFieldDto(field, modelName, models, overrides) {
|
|
1399
|
+
const override = fieldOverride(overrides, modelName, field.name);
|
|
1400
|
+
return {
|
|
1401
|
+
name: field.name,
|
|
1402
|
+
kind: field.kind,
|
|
1403
|
+
isId: field.isId,
|
|
1404
|
+
isRequired: field.isRequired,
|
|
1405
|
+
isUnique: field.isUnique,
|
|
1406
|
+
isList: field.isList,
|
|
1407
|
+
isGenerated: field.isGenerated,
|
|
1408
|
+
readOnly: isReadOnly(overrides, modelName, field),
|
|
1409
|
+
...field.writeOnly === true ? {
|
|
1410
|
+
writeOnly: true
|
|
1411
|
+
} : {},
|
|
1412
|
+
...override?.label !== void 0 ? {
|
|
1413
|
+
label: override.label
|
|
1414
|
+
} : {},
|
|
1415
|
+
...override?.widget !== void 0 ? {
|
|
1416
|
+
widget: override.widget
|
|
1417
|
+
} : {},
|
|
1418
|
+
...field.defaultValue !== void 0 ? {
|
|
1419
|
+
defaultValue: field.defaultValue
|
|
1420
|
+
} : {},
|
|
1421
|
+
...field.enumValues ? {
|
|
1422
|
+
enumValues: [
|
|
1423
|
+
...field.enumValues
|
|
1424
|
+
]
|
|
1425
|
+
} : {},
|
|
1426
|
+
...field.relation ? {
|
|
1427
|
+
relation: {
|
|
1428
|
+
targetModel: field.relation.targetModel,
|
|
1429
|
+
cardinality: field.relation.cardinality,
|
|
1430
|
+
...field.relation.from !== void 0 ? {
|
|
1431
|
+
from: field.relation.from
|
|
1432
|
+
} : {},
|
|
1433
|
+
...field.relation.to !== void 0 ? {
|
|
1434
|
+
to: field.relation.to
|
|
1435
|
+
} : {},
|
|
1436
|
+
...relationShape(field, models) !== void 0 ? {
|
|
1437
|
+
shape: relationShape(field, models)
|
|
1438
|
+
} : {},
|
|
1439
|
+
...detachBlockedReason(field, models) !== void 0 ? {
|
|
1440
|
+
detachBlocked: detachBlockedReason(field, models)
|
|
1441
|
+
} : {},
|
|
1442
|
+
...targetForeignKeyOf(field, models) !== void 0 ? {
|
|
1443
|
+
targetForeignKey: targetForeignKeyOf(field, models)
|
|
1444
|
+
} : {}
|
|
1445
|
+
}
|
|
1446
|
+
} : {}
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
__name(toFieldDto, "toFieldDto");
|
|
1450
|
+
function toMetadataDto(models, overrides, permissions, actions) {
|
|
1451
|
+
const present = new Set(models.map((model) => model.name));
|
|
1452
|
+
return {
|
|
1453
|
+
models: byOrder(models, (model) => overrides?.[model.name]?.order).map((model) => ({
|
|
1454
|
+
name: model.name,
|
|
1455
|
+
primaryKey: [
|
|
1456
|
+
...model.primaryKey
|
|
1457
|
+
],
|
|
1458
|
+
displayField: displayFieldFor(model),
|
|
1459
|
+
can: permissions?.get(model.name) ?? ALL_PERMITTED,
|
|
1460
|
+
actions: actions?.get(model.name) ?? [],
|
|
1461
|
+
...overrides?.[model.name]?.label !== void 0 ? {
|
|
1462
|
+
label: overrides[model.name]?.label
|
|
1463
|
+
} : {},
|
|
1464
|
+
...overrides?.[model.name]?.icon !== void 0 ? {
|
|
1465
|
+
icon: overrides[model.name]?.icon
|
|
1466
|
+
} : {},
|
|
1467
|
+
fields: byOrder(model.fields.filter((field) => !field.relation || present.has(field.relation.targetModel)), (field) => fieldOverride(overrides, model.name, field.name)?.order).map((field) => toFieldDto(field, model.name, models, overrides))
|
|
1468
|
+
}))
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
__name(toMetadataDto, "toMetadataDto");
|
|
1472
|
+
|
|
1473
|
+
// src/admin/service.ts
|
|
1474
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
1475
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1476
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
1477
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
1478
|
+
} else {
|
|
1479
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1480
|
+
if (d = decorators[i]) {
|
|
1481
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1486
|
+
}
|
|
1487
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
1488
|
+
function _ts_metadata2(metadataKey, metadataValue) {
|
|
1489
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
1490
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
1494
|
+
function _ts_param2(paramIndex, decorator) {
|
|
1495
|
+
return function(target, key) {
|
|
1496
|
+
decorator(target, key, paramIndex);
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
__name(_ts_param2, "_ts_param");
|
|
1500
|
+
function readableFields(model) {
|
|
1501
|
+
return model.fields.filter((field) => field.writeOnly !== true);
|
|
1502
|
+
}
|
|
1503
|
+
__name(readableFields, "readableFields");
|
|
1504
|
+
var MAX_BULK_DELETE = 200;
|
|
1505
|
+
var AdminService = class {
|
|
1506
|
+
static {
|
|
1507
|
+
__name(this, "AdminService");
|
|
1508
|
+
}
|
|
1509
|
+
adapter;
|
|
1510
|
+
resourceAuth;
|
|
1511
|
+
resources;
|
|
1512
|
+
overrides;
|
|
1513
|
+
hooks;
|
|
1514
|
+
actions;
|
|
1515
|
+
auth;
|
|
1516
|
+
dashboard;
|
|
1517
|
+
constructor(adapter, resourceAuth, resources, overrides, hooks, actions, auth, dashboard) {
|
|
1518
|
+
this.adapter = adapter;
|
|
1519
|
+
this.resourceAuth = resourceAuth;
|
|
1520
|
+
this.resources = resources;
|
|
1521
|
+
this.overrides = overrides;
|
|
1522
|
+
this.hooks = hooks;
|
|
1523
|
+
this.actions = actions;
|
|
1524
|
+
this.auth = auth;
|
|
1525
|
+
this.dashboard = dashboard;
|
|
1526
|
+
}
|
|
1527
|
+
logger = new import_common6.Logger("NestAdmin");
|
|
1528
|
+
/**
|
|
1529
|
+
* Fail at boot on a selection that names a model the schema does not have.
|
|
1530
|
+
*
|
|
1531
|
+
* A typo in `exclude` leaves the model exposed - the opposite of what was
|
|
1532
|
+
* asked for, and invisible until someone finds the table in the admin. It
|
|
1533
|
+
* cannot be checked in `forRoot`, because the model list comes from the
|
|
1534
|
+
* adapter and asking for it is asynchronous; this is the first moment it can
|
|
1535
|
+
* be known, and it is still before the first request.
|
|
1536
|
+
*/
|
|
1537
|
+
async onModuleInit() {
|
|
1538
|
+
const schema = await this.adapter.getModels();
|
|
1539
|
+
const known = schema.map((model) => model.name);
|
|
1540
|
+
const missingResources = unknownSelectionNames(schema, this.resources);
|
|
1541
|
+
if (missingResources.length > 0) {
|
|
1542
|
+
throw new Error(`AdminModule \`resources\` names ${missingResources.length === 1 ? "a model" : "models"} that the schema does not have: ${missingResources.join(", ")}. Known models: ${known.join(", ")}.`);
|
|
1543
|
+
}
|
|
1544
|
+
const missingOverrides = unknownOverrideNames(selectModels(schema, this.resources), this.overrides);
|
|
1545
|
+
if (missingOverrides.length > 0) {
|
|
1546
|
+
throw new Error(`AdminModule \`models\` names ${missingOverrides.length === 1 ? "a model or field" : "models or fields"} this admin does not have: ${missingOverrides.join(", ")}. A typo in \`hidden\` leaves the real column exposed, so this is an error rather than a warning.`);
|
|
1547
|
+
}
|
|
1548
|
+
await this.checkBuiltInAuth(selectModels(schema, this.resources));
|
|
1549
|
+
const unwritable = unwritableHiddenFields(selectModels(schema, this.resources), this.overrides);
|
|
1550
|
+
if (unwritable.length > 0) {
|
|
1551
|
+
const one = unwritable.length === 1;
|
|
1552
|
+
throw new Error(`AdminModule \`models\` hides ${unwritable.join(", ")}, ${one ? "which is a required field" : "which are required fields"} with no default. Hiding ${one ? "it" : "them"} leaves no way to supply a value, so every create would fail. Give the column a default, make it optional, or leave it visible.`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Two things about the built-in authentication that are only knowable here.
|
|
1557
|
+
*
|
|
1558
|
+
* Warnings rather than boot failures, and the distinction is deliberate.
|
|
1559
|
+
* Both describe a *deployment* that is wrong rather than a configuration
|
|
1560
|
+
* that cannot work - and an admin that refuses to start because its account
|
|
1561
|
+
* table is empty is an admin nobody can seed, because the seed script
|
|
1562
|
+
* imports the module.
|
|
1563
|
+
*/
|
|
1564
|
+
async checkBuiltInAuth(exposed) {
|
|
1565
|
+
const runtime = builtInRuntimeOf(this.auth);
|
|
1566
|
+
if (!runtime) return;
|
|
1567
|
+
const accountModel = runtime.store.describes;
|
|
1568
|
+
if (accountModel !== void 0 && exposed.some((model) => model.name === accountModel)) {
|
|
1569
|
+
this.logger.warn(`AdminModule exposes "${accountModel}" as a resource, and it is also where the admin keeps its own accounts. Anyone who may edit it can grant themselves anything the admin can do. Exclude it with resources: { exclude: ["${accountModel}"] }.`);
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
if (await runtime.store.count() === 0) {
|
|
1573
|
+
this.logger.warn("AdminModule is using builtInAuth() and the account store is empty, so nobody can sign in. Create the first account with hashAdminPassword().");
|
|
1574
|
+
}
|
|
1575
|
+
} catch (cause) {
|
|
1576
|
+
this.logger.warn(`Could not read the admin account store: ${String(cause)}`);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* What the dashboard shows this principal.
|
|
1581
|
+
*
|
|
1582
|
+
* Authorized the way everything else is, and *before* anything is queried: a
|
|
1583
|
+
* widget over a model this principal may not list is absent from the
|
|
1584
|
+
* document, so a dashboard cannot become a way to count rows of a table
|
|
1585
|
+
* nobody would let you open.
|
|
1586
|
+
*
|
|
1587
|
+
* The exposed model list is passed in rather than looked up again inside, so
|
|
1588
|
+
* "which models does this person see" is answered once, here, by the same
|
|
1589
|
+
* code that answers it for the metadata document.
|
|
1590
|
+
*/
|
|
1591
|
+
async getDashboard(context) {
|
|
1592
|
+
const models = await this.exposedModels();
|
|
1593
|
+
const permitted = [];
|
|
1594
|
+
for (const model of models) {
|
|
1595
|
+
if (await this.permits(context, model.name, "list")) permitted.push(model);
|
|
1596
|
+
}
|
|
1597
|
+
return buildDashboard({
|
|
1598
|
+
adapter: this.adapter,
|
|
1599
|
+
models: permitted,
|
|
1600
|
+
declared: this.dashboard,
|
|
1601
|
+
context,
|
|
1602
|
+
labels: Object.fromEntries(Object.entries(this.overrides ?? {}).map(([name, override]) => [
|
|
1603
|
+
name,
|
|
1604
|
+
override?.label
|
|
1605
|
+
]))
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* The public metadata document a frontend renders resources from.
|
|
1610
|
+
*
|
|
1611
|
+
* Models the principal may not see are filtered out **before** mapping, so a
|
|
1612
|
+
* denied model never reaches the DTO at all - not its name, fields, relations,
|
|
1613
|
+
* primary key or enum values. The response is not "everything, minus some";
|
|
1614
|
+
* it is a description of the schema this principal has.
|
|
1615
|
+
*/
|
|
1616
|
+
async getMetadata(context) {
|
|
1617
|
+
const models = await this.exposedModels();
|
|
1618
|
+
const visible = [];
|
|
1619
|
+
for (const model of models) {
|
|
1620
|
+
if (await this.isVisible(context, model.name)) visible.push(model);
|
|
1621
|
+
}
|
|
1622
|
+
return toMetadataDto(visible, this.overrides, await this.permissionsFor(context, visible), await this.actionsFor(context, visible));
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* List records.
|
|
1626
|
+
*
|
|
1627
|
+
* Metadata is resolved first - it decides whether the model is part of this
|
|
1628
|
+
* admin at all - and authorization second, so a denied model still never
|
|
1629
|
+
* reaches `adapter.list`. Query parsing needs that metadata anyway: only the
|
|
1630
|
+
* schema knows whether `price` should arrive as a number or a string.
|
|
1631
|
+
*/
|
|
1632
|
+
async list(context, model, rawQuery) {
|
|
1633
|
+
const metadata = await this.requireModel(model);
|
|
1634
|
+
await this.assertAllowed(context, model, "list");
|
|
1635
|
+
return this.projectPage(metadata, await this.adapter.list(model, this.scopeToFields(metadata, parseListQuery(rawQuery, metadata))));
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Fetch one record.
|
|
1639
|
+
*
|
|
1640
|
+
* The adapter returns `null` for a missing record; over HTTP that is a 404,
|
|
1641
|
+
* so it is turned into an error here rather than in the controller.
|
|
1642
|
+
*/
|
|
1643
|
+
async findOne(context, model, id) {
|
|
1644
|
+
const metadata = await this.requireModel(model);
|
|
1645
|
+
await this.assertAllowed(context, model, "read");
|
|
1646
|
+
const record = await this.adapter.findOne(model, id);
|
|
1647
|
+
if (record === null) throw new RecordNotFoundError(model, id);
|
|
1648
|
+
return this.project(metadata, record);
|
|
1649
|
+
}
|
|
1650
|
+
async create(context, model, data) {
|
|
1651
|
+
const metadata = await this.requireModel(model);
|
|
1652
|
+
await this.assertAllowed(context, model, "create");
|
|
1653
|
+
this.assertWritable(metadata, data);
|
|
1654
|
+
const prepared = await this.runBefore(context, metadata, "beforeCreate", data);
|
|
1655
|
+
const created = await this.adapter.create(model, prepared);
|
|
1656
|
+
await this.runAfter(context, model, "afterCreate", {
|
|
1657
|
+
record: created
|
|
1658
|
+
});
|
|
1659
|
+
return this.project(metadata, created);
|
|
1660
|
+
}
|
|
1661
|
+
async update(context, model, id, data) {
|
|
1662
|
+
const metadata = await this.requireModel(model);
|
|
1663
|
+
await this.assertAllowed(context, model, "update");
|
|
1664
|
+
this.assertWritable(metadata, data);
|
|
1665
|
+
const prepared = await this.runBefore(context, metadata, "beforeUpdate", data, id);
|
|
1666
|
+
const updated = await this.adapter.update(model, id, prepared);
|
|
1667
|
+
await this.runAfter(context, model, "afterUpdate", {
|
|
1668
|
+
id,
|
|
1669
|
+
record: updated
|
|
1670
|
+
});
|
|
1671
|
+
return this.project(metadata, updated);
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Delete several records, and say what happened to each.
|
|
1675
|
+
*
|
|
1676
|
+
* ## Why this is a loop and not a `deleteMany`
|
|
1677
|
+
*
|
|
1678
|
+
* The adapter contract has no bulk delete, and giving it one would mean
|
|
1679
|
+
* every adapter had to have one. More to the point, hooks are per-record: an
|
|
1680
|
+
* application that refuses to delete a pinned post must still refuse it when
|
|
1681
|
+
* the post is one of forty checkboxes. A single `deleteMany` would step past
|
|
1682
|
+
* every one of those refusals at once, which is the opposite of what a
|
|
1683
|
+
* confirmation dialog leads someone to expect.
|
|
1684
|
+
*
|
|
1685
|
+
* ## Why a partial result is a success
|
|
1686
|
+
*
|
|
1687
|
+
* Deleting thirty records where two are still referenced is not a failed
|
|
1688
|
+
* request - twenty-eight rows are gone, and an error response would say
|
|
1689
|
+
* nothing about which. So the response is a 200 carrying both lists, and the
|
|
1690
|
+
* interface reports them. Nothing is rolled back, and `§ Known Limitations`
|
|
1691
|
+
* says so: this is not a transaction, exactly as hooks are not.
|
|
1692
|
+
*/
|
|
1693
|
+
async deleteMany(context, model, ids) {
|
|
1694
|
+
await this.requireModel(model);
|
|
1695
|
+
await this.assertAllowed(context, model, "delete");
|
|
1696
|
+
if (ids.length === 0) {
|
|
1697
|
+
throw new InvalidQueryError('Deleting records requires a body of the form { "ids": [...] }.');
|
|
1698
|
+
}
|
|
1699
|
+
if (ids.length > MAX_BULK_DELETE) {
|
|
1700
|
+
throw new InvalidQueryError(`Refusing to delete ${ids.length} records in one request. The limit is ${MAX_BULK_DELETE}.`);
|
|
1701
|
+
}
|
|
1702
|
+
const before = this.hooks?.[model]?.beforeDelete;
|
|
1703
|
+
const deleted = [];
|
|
1704
|
+
const failed = [];
|
|
1705
|
+
for (const id of ids) {
|
|
1706
|
+
try {
|
|
1707
|
+
if (before) await before({
|
|
1708
|
+
context,
|
|
1709
|
+
model,
|
|
1710
|
+
id
|
|
1711
|
+
});
|
|
1712
|
+
await this.adapter.delete(model, id);
|
|
1713
|
+
await this.runAfter(context, model, "afterDelete", {
|
|
1714
|
+
id
|
|
1715
|
+
});
|
|
1716
|
+
deleted.push(id);
|
|
1717
|
+
} catch (cause) {
|
|
1718
|
+
failed.push({
|
|
1719
|
+
id,
|
|
1720
|
+
message: clientMessage(cause)
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
deleted,
|
|
1726
|
+
failed
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
async delete(context, model, id) {
|
|
1730
|
+
await this.requireModel(model);
|
|
1731
|
+
await this.assertAllowed(context, model, "delete");
|
|
1732
|
+
const before = this.hooks?.[model]?.beforeDelete;
|
|
1733
|
+
if (before) await before({
|
|
1734
|
+
context,
|
|
1735
|
+
model,
|
|
1736
|
+
id
|
|
1737
|
+
});
|
|
1738
|
+
await this.adapter.delete(model, id);
|
|
1739
|
+
await this.runAfter(context, model, "afterDelete", {
|
|
1740
|
+
id
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* A page of the records on the far side of a to-many relation.
|
|
1745
|
+
*
|
|
1746
|
+
* Authorized against **both** models, and the distinction matters. Reading
|
|
1747
|
+
* `/User/u1/posts` returns Post records, so a principal who may read a User
|
|
1748
|
+
* but not list Posts must not receive them through the back door of a
|
|
1749
|
+
* relation. The parent decides whether this record may be opened at all; the
|
|
1750
|
+
* target decides whether its records may be listed.
|
|
1751
|
+
*/
|
|
1752
|
+
async listRelated(context, model, id, relationField, rawQuery) {
|
|
1753
|
+
const parent = await this.requireModel(model);
|
|
1754
|
+
await this.assertAllowed(context, model, "read");
|
|
1755
|
+
const target = await this.requireRelationTarget(parent, relationField);
|
|
1756
|
+
await this.assertAllowed(context, target.name, "list");
|
|
1757
|
+
return this.projectPage(target, await this.adapter.listRelated(model, id, relationField, this.scopeToFields(target, parseListQuery(rawQuery, target))));
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Link an existing record to this one.
|
|
1761
|
+
*
|
|
1762
|
+
* Requires `update` on both models. Across a one-to-many the child's foreign
|
|
1763
|
+
* key is what actually changes, so permitting this with rights over the
|
|
1764
|
+
* parent alone would let someone edit records they cannot otherwise touch.
|
|
1765
|
+
*/
|
|
1766
|
+
async attachRelated(context, model, id, relationField, targetId) {
|
|
1767
|
+
const target = await this.assertMayRelink(context, model, relationField);
|
|
1768
|
+
await this.assertAllowed(context, target.name, "update");
|
|
1769
|
+
await this.adapter.attachRelated(model, id, relationField, targetId);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Unlink a record from this one, leaving both in place.
|
|
1773
|
+
*
|
|
1774
|
+
* Refused up front when the relation cannot be broken - a child whose foreign
|
|
1775
|
+
* key is required cannot exist without a parent, so there is nothing to
|
|
1776
|
+
* detach it to. Saying so is better than forwarding a constraint violation.
|
|
1777
|
+
*/
|
|
1778
|
+
async detachRelated(context, model, id, relationField, targetId) {
|
|
1779
|
+
const target = await this.assertMayRelink(context, model, relationField);
|
|
1780
|
+
await this.assertAllowed(context, target.name, "update");
|
|
1781
|
+
const parent = await this.requireModel(model);
|
|
1782
|
+
const field = parent.fields.find((candidate) => candidate.name === relationField);
|
|
1783
|
+
const blocked = field ? detachBlockedReason(field, await this.exposedModels()) : void 0;
|
|
1784
|
+
if (blocked) throw new InvalidQueryError(blocked);
|
|
1785
|
+
await this.adapter.detachRelated(model, id, relationField, targetId);
|
|
1786
|
+
}
|
|
1787
|
+
/** Shared preamble for attach and detach: the parent must be updatable. */
|
|
1788
|
+
async assertMayRelink(context, model, relationField) {
|
|
1789
|
+
const parent = await this.requireModel(model);
|
|
1790
|
+
await this.assertAllowed(context, model, "update");
|
|
1791
|
+
return this.requireRelationTarget(parent, relationField);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* The model on the far side of a to-many relation field.
|
|
1795
|
+
*
|
|
1796
|
+
* Resolved through the exposed set, so a relation pointing at a model this
|
|
1797
|
+
* admin does not expose reads as an unknown field rather than as a route
|
|
1798
|
+
* into it.
|
|
1799
|
+
*/
|
|
1800
|
+
async requireRelationTarget(parent, relationField) {
|
|
1801
|
+
const field = parent.fields.find((candidate) => candidate.name === relationField);
|
|
1802
|
+
if (!field?.relation || field.relation.cardinality !== "many") {
|
|
1803
|
+
throw new FieldNotFoundError(parent.name, relationField, "Only a to-many relation can be listed this way.");
|
|
1804
|
+
}
|
|
1805
|
+
const target = (await this.exposedModels()).find((candidate) => candidate.name === field.relation?.targetModel);
|
|
1806
|
+
if (!target) throw new FieldNotFoundError(parent.name, relationField);
|
|
1807
|
+
return target;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* A record as this admin is allowed to return it.
|
|
1811
|
+
*
|
|
1812
|
+
* A whitelist against the effective metadata, which is what makes `hidden`
|
|
1813
|
+
* a guarantee rather than a request. The adapter reads whole rows - it knows
|
|
1814
|
+
* nothing about admin configuration - so a hidden column arrives here and is
|
|
1815
|
+
* dropped before anything can serialise it.
|
|
1816
|
+
*
|
|
1817
|
+
* Whitelisting rather than deleting the hidden names also covers a column the
|
|
1818
|
+
* adapter reports that the metadata does not describe: if it is not part of
|
|
1819
|
+
* this admin, it does not leave it.
|
|
1820
|
+
*/
|
|
1821
|
+
project(model, record) {
|
|
1822
|
+
const allowed = new Set(readableFields(model).map((field) => field.name));
|
|
1823
|
+
const projected = {};
|
|
1824
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1825
|
+
if (allowed.has(key)) projected[key] = value;
|
|
1826
|
+
}
|
|
1827
|
+
return projected;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Tell the adapter which fields this admin exposes.
|
|
1831
|
+
*
|
|
1832
|
+
* The adapter reads a schema, not a configuration, so without this a hidden
|
|
1833
|
+
* column would still be searched by free text, sorted and filtered on, and
|
|
1834
|
+
* read from the database - each of them a way to learn a value nobody is
|
|
1835
|
+
* meant to see. `project` would still keep it out of the response, but
|
|
1836
|
+
* "you cannot read it" is a weaker promise than "it was never fetched".
|
|
1837
|
+
*/
|
|
1838
|
+
/**
|
|
1839
|
+
* Which columns the adapter is allowed to return.
|
|
1840
|
+
*
|
|
1841
|
+
* Not every field the model has: a `writeOnly` one is accepted on a write and
|
|
1842
|
+
* must never come back, so it is left out of the query itself rather than
|
|
1843
|
+
* removed from the answer afterwards. The projection below removes it a
|
|
1844
|
+
* second time, which is deliberate - see `FieldMetadata.writeOnly`.
|
|
1845
|
+
*/
|
|
1846
|
+
scopeToFields(model, query) {
|
|
1847
|
+
return {
|
|
1848
|
+
...query,
|
|
1849
|
+
fields: readableFields(model).map((field) => field.name)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
projectPage(model, page) {
|
|
1853
|
+
return {
|
|
1854
|
+
...page,
|
|
1855
|
+
data: page.data.map((record) => this.project(model, record))
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Reject a write that names a field this admin will not write.
|
|
1860
|
+
*
|
|
1861
|
+
* The adapter validates too, but against the *schema* - it would accept a
|
|
1862
|
+
* hidden or read-only column, because from where it stands those are ordinary
|
|
1863
|
+
* writable ones. This is the only layer that knows the difference.
|
|
1864
|
+
*/
|
|
1865
|
+
assertWritable(model, data) {
|
|
1866
|
+
for (const key of Object.keys(data)) {
|
|
1867
|
+
const field = model.fields.find((candidate) => candidate.name === key);
|
|
1868
|
+
if (!field) throw new FieldNotFoundError(model.name, key);
|
|
1869
|
+
if (isReadOnly(this.overrides, model.name, field)) {
|
|
1870
|
+
throw new FieldNotFoundError(model.name, key, field.isGenerated ? "This value is produced by the database." : "This field is configured as read-only.");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* What this principal may do with each visible model.
|
|
1876
|
+
*
|
|
1877
|
+
* Asked of the same policy the requests go through, so the document and the
|
|
1878
|
+
* enforcement cannot disagree. A policy that throws `ForbiddenError` is read
|
|
1879
|
+
* as a denial, exactly as `isVisible` reads it; anything else it throws is a
|
|
1880
|
+
* bug and propagates.
|
|
1881
|
+
*
|
|
1882
|
+
* Without this the interface offers `New`, `Edit` and `Delete` to a
|
|
1883
|
+
* principal for whom every one of them would be refused - a button that
|
|
1884
|
+
* exists only to produce a 403 is worse than no button.
|
|
1885
|
+
*/
|
|
1886
|
+
async permissionsFor(context, models) {
|
|
1887
|
+
const permissions = /* @__PURE__ */ new Map();
|
|
1888
|
+
for (const model of models) {
|
|
1889
|
+
const permits = /* @__PURE__ */ __name(async (operation) => this.permits(context, model.name, operation), "permits");
|
|
1890
|
+
permissions.set(model.name, {
|
|
1891
|
+
list: await permits("list"),
|
|
1892
|
+
read: await permits("read"),
|
|
1893
|
+
create: await permits("create"),
|
|
1894
|
+
update: await permits("update"),
|
|
1895
|
+
delete: await permits("delete")
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return permissions;
|
|
1899
|
+
}
|
|
1900
|
+
/** The policy's answer for one operation, with a thrown denial read as `false`. */
|
|
1901
|
+
async permits(context, model, operation) {
|
|
1902
|
+
try {
|
|
1903
|
+
return await this.resourceAuth.authorize({
|
|
1904
|
+
context,
|
|
1905
|
+
model,
|
|
1906
|
+
operation
|
|
1907
|
+
}) !== false;
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
if (isNestAdminError(error) && error.kind === "forbidden") return false;
|
|
1910
|
+
throw error;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Run a `before` hook, if the model has one.
|
|
1915
|
+
*
|
|
1916
|
+
* The result is validated again rather than trusted: a hook is application
|
|
1917
|
+
* code, and the rule that a hidden or read-only field cannot be written is
|
|
1918
|
+
* not one it should be able to step around by accident.
|
|
1919
|
+
*/
|
|
1920
|
+
async runBefore(context, metadata, hook, data, id) {
|
|
1921
|
+
const handler = this.hooks?.[metadata.name]?.[hook];
|
|
1922
|
+
if (!handler) return data;
|
|
1923
|
+
const result = await (hook === "beforeCreate" ? handler({
|
|
1924
|
+
context,
|
|
1925
|
+
model: metadata.name,
|
|
1926
|
+
data
|
|
1927
|
+
}) : handler({
|
|
1928
|
+
context,
|
|
1929
|
+
model: metadata.name,
|
|
1930
|
+
id,
|
|
1931
|
+
data
|
|
1932
|
+
}));
|
|
1933
|
+
this.assertWritable(metadata, result);
|
|
1934
|
+
return result;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Run an `after` hook, if the model has one.
|
|
1938
|
+
*
|
|
1939
|
+
* Nothing is rolled back if it throws - the write already happened - so the
|
|
1940
|
+
* failure is reported as it is rather than dressed up as a failed write.
|
|
1941
|
+
*/
|
|
1942
|
+
async runAfter(context, model, hook, args) {
|
|
1943
|
+
const handler = this.hooks?.[model]?.[hook];
|
|
1944
|
+
if (!handler) return;
|
|
1945
|
+
await handler({
|
|
1946
|
+
context,
|
|
1947
|
+
model,
|
|
1948
|
+
...args
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* The actions this principal may run, per model.
|
|
1953
|
+
*
|
|
1954
|
+
* Filtered by the policy before it reaches the document, so an action that
|
|
1955
|
+
* would be refused is simply not there - the interface cannot draw a button
|
|
1956
|
+
* for something it was never told about.
|
|
1957
|
+
*/
|
|
1958
|
+
async actionsFor(context, models) {
|
|
1959
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
1960
|
+
for (const model of models) {
|
|
1961
|
+
const declared = this.actions?.[model.name] ?? [];
|
|
1962
|
+
if (declared.length === 0) continue;
|
|
1963
|
+
if (!await this.permits(context, model.name, "action")) continue;
|
|
1964
|
+
byModel.set(model.name, declared.map((action) => ({
|
|
1965
|
+
name: action.name,
|
|
1966
|
+
label: action.label ?? action.name,
|
|
1967
|
+
scope: action.scope,
|
|
1968
|
+
...action.confirm !== void 0 ? {
|
|
1969
|
+
confirm: action.confirm
|
|
1970
|
+
} : {},
|
|
1971
|
+
...action.danger !== void 0 ? {
|
|
1972
|
+
danger: action.danger
|
|
1973
|
+
} : {}
|
|
1974
|
+
})));
|
|
1975
|
+
}
|
|
1976
|
+
return byModel;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Run one application-defined action.
|
|
1980
|
+
*
|
|
1981
|
+
* Authorized as `'action'` rather than as the operation it resembles: an
|
|
1982
|
+
* action can do anything, so a policy should be able to decide about it on
|
|
1983
|
+
* its own terms.
|
|
1984
|
+
*
|
|
1985
|
+
* A `'record'` action is given the id; a `'list'` one is not, and passing an
|
|
1986
|
+
* id to it - or omitting one from a record action - is a request that does
|
|
1987
|
+
* not match the action that was declared.
|
|
1988
|
+
*/
|
|
1989
|
+
async runAction(context, model, name, id) {
|
|
1990
|
+
await this.requireModel(model);
|
|
1991
|
+
await this.assertAllowed(context, model, "action");
|
|
1992
|
+
const action = (this.actions?.[model] ?? []).find((candidate) => candidate.name === name);
|
|
1993
|
+
if (!action) {
|
|
1994
|
+
throw new FieldNotFoundError(model, name, "No such action.");
|
|
1995
|
+
}
|
|
1996
|
+
if (action.scope === "record" && id === void 0) {
|
|
1997
|
+
throw new InvalidQueryError(`Action "${name}" applies to one record and needs an id.`);
|
|
1998
|
+
}
|
|
1999
|
+
if (action.scope === "list" && id !== void 0) {
|
|
2000
|
+
throw new InvalidQueryError(`Action "${name}" applies to the whole model, not to a record.`);
|
|
2001
|
+
}
|
|
2002
|
+
return await action.run({
|
|
2003
|
+
context,
|
|
2004
|
+
model,
|
|
2005
|
+
...id === void 0 ? {} : {
|
|
2006
|
+
id
|
|
2007
|
+
}
|
|
2008
|
+
}) ?? {};
|
|
2009
|
+
}
|
|
2010
|
+
// ------------------------------------------------------- resource policy
|
|
2011
|
+
/**
|
|
2012
|
+
* Deny the request unless the policy permits this operation on this model.
|
|
2013
|
+
*
|
|
2014
|
+
* Called before any adapter operation. Both a `false` return and a thrown
|
|
2015
|
+
* `ForbiddenError` mean the same thing here, so a host may use whichever
|
|
2016
|
+
* reads better. Anything else the policy throws propagates untouched and the
|
|
2017
|
+
* exception filter turns it into a generic 500 - a broken policy fails the
|
|
2018
|
+
* request rather than quietly allowing it.
|
|
2019
|
+
*/
|
|
2020
|
+
async assertAllowed(context, model, operation) {
|
|
2021
|
+
const decision = await this.resourceAuth.authorize({
|
|
2022
|
+
context,
|
|
2023
|
+
model,
|
|
2024
|
+
operation
|
|
2025
|
+
});
|
|
2026
|
+
if (decision === false) throw new ForbiddenError();
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Is this model visible in the metadata document?
|
|
2030
|
+
*
|
|
2031
|
+
* Same policy, different consequence. A denial here must **hide** the model
|
|
2032
|
+
* rather than fail the request: surfacing a 403 from `GET /admin/meta` would
|
|
2033
|
+
* tell the caller that a model they cannot see exists, which is the side
|
|
2034
|
+
* channel this whole phase is meant to close.
|
|
2035
|
+
*
|
|
2036
|
+
* A `ForbiddenError` is therefore caught and read as "not visible". Any other
|
|
2037
|
+
* error is rethrown - a bug in the policy must surface as a 500, not silently
|
|
2038
|
+
* reshape the schema a client is shown.
|
|
2039
|
+
*/
|
|
2040
|
+
async isVisible(context, model) {
|
|
2041
|
+
try {
|
|
2042
|
+
const decision = await this.resourceAuth.authorize({
|
|
2043
|
+
context,
|
|
2044
|
+
model,
|
|
2045
|
+
operation: "metadata"
|
|
2046
|
+
});
|
|
2047
|
+
return decision !== false;
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
if (isNestAdminError(error) && error.kind === "forbidden") return false;
|
|
2050
|
+
throw error;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* The models this admin exposes, after the configured selection.
|
|
2055
|
+
*
|
|
2056
|
+
* Every path goes through here, so an excluded model is absent from the
|
|
2057
|
+
* metadata document and unknown to every route.
|
|
2058
|
+
*/
|
|
2059
|
+
async exposedModels() {
|
|
2060
|
+
return applyOverrides(selectModels(await this.adapter.getModels(), this.resources), this.overrides);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Resolve a model name to its metadata, or fail with 404.
|
|
2064
|
+
*
|
|
2065
|
+
* Called before the policy on every operation, and that order is deliberate.
|
|
2066
|
+
* Whether a model exists is structural - the same answer for everyone - so an
|
|
2067
|
+
* excluded model answers 404 rather than 403, and does so identically for
|
|
2068
|
+
* every principal. Asking the policy first would make a model that is not
|
|
2069
|
+
* part of this admin look like one the caller merely lacks access to.
|
|
2070
|
+
*/
|
|
2071
|
+
async requireModel(model) {
|
|
2072
|
+
const models = await this.exposedModels();
|
|
2073
|
+
const found = models.find((candidate) => candidate.name === model);
|
|
2074
|
+
if (!found) {
|
|
2075
|
+
throw new ModelNotFoundError(model, models.map((candidate) => candidate.name));
|
|
2076
|
+
}
|
|
2077
|
+
return found;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
AdminService = _ts_decorate3([
|
|
2081
|
+
(0, import_common6.Injectable)(),
|
|
2082
|
+
_ts_param2(0, (0, import_common6.Inject)(ADMIN_ADAPTER)),
|
|
2083
|
+
_ts_param2(1, (0, import_common6.Inject)(ADMIN_RESOURCE_AUTH)),
|
|
2084
|
+
_ts_param2(2, (0, import_common6.Inject)(ADMIN_RESOURCES)),
|
|
2085
|
+
_ts_param2(3, (0, import_common6.Inject)(ADMIN_MODELS)),
|
|
2086
|
+
_ts_param2(4, (0, import_common6.Inject)(ADMIN_HOOKS)),
|
|
2087
|
+
_ts_param2(5, (0, import_common6.Inject)(ADMIN_ACTIONS)),
|
|
2088
|
+
_ts_param2(6, (0, import_common6.Inject)(ADMIN_AUTH)),
|
|
2089
|
+
_ts_param2(7, (0, import_common6.Inject)(ADMIN_DASHBOARD)),
|
|
2090
|
+
_ts_metadata2("design:type", Function),
|
|
2091
|
+
_ts_metadata2("design:paramtypes", [
|
|
2092
|
+
typeof OrmAdapter === "undefined" ? Object : OrmAdapter,
|
|
2093
|
+
typeof AdminResourceAuth === "undefined" ? Object : AdminResourceAuth,
|
|
2094
|
+
Object,
|
|
2095
|
+
Object,
|
|
2096
|
+
Object,
|
|
2097
|
+
Object,
|
|
2098
|
+
typeof AdminAuth === "undefined" ? Object : AdminAuth,
|
|
2099
|
+
Object
|
|
2100
|
+
])
|
|
2101
|
+
], AdminService);
|
|
2102
|
+
|
|
2103
|
+
// src/admin/controller.ts
|
|
2104
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
2105
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2106
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
2107
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
2108
|
+
} else {
|
|
2109
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
2110
|
+
if (d = decorators[i]) {
|
|
2111
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2116
|
+
}
|
|
2117
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
2118
|
+
function _ts_metadata3(metadataKey, metadataValue) {
|
|
2119
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
2120
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
2124
|
+
function _ts_param3(paramIndex, decorator) {
|
|
2125
|
+
return function(target, key) {
|
|
2126
|
+
decorator(target, key, paramIndex);
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
__name(_ts_param3, "_ts_param");
|
|
2130
|
+
var AdminController = class {
|
|
2131
|
+
static {
|
|
2132
|
+
__name(this, "AdminController");
|
|
2133
|
+
}
|
|
2134
|
+
service;
|
|
2135
|
+
constructor(service) {
|
|
2136
|
+
this.service = service;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Declared before `:model` so the literal segment wins. A model named
|
|
2140
|
+
* exactly `meta` would be shadowed; Prisma model names are conventionally
|
|
2141
|
+
* capitalised (`Meta`) and matching is case-sensitive, so this is a narrow
|
|
2142
|
+
* and documented corner.
|
|
2143
|
+
*/
|
|
2144
|
+
/**
|
|
2145
|
+
* `POST /admin/actions/:model/:action[/:id]` - run an application action.
|
|
2146
|
+
*
|
|
2147
|
+
* Under a reserved first segment, and declared before every `:model` route,
|
|
2148
|
+
* so `actions` is matched literally. The same arrangement already reserves
|
|
2149
|
+
* `meta` here and `assets` in the UI controller; the cost is that a model
|
|
2150
|
+
* called `actions` would be unreachable, which is documented rather than
|
|
2151
|
+
* guarded against.
|
|
2152
|
+
*/
|
|
2153
|
+
async runListAction(context, model, action) {
|
|
2154
|
+
return success(await this.service.runAction(context, model, action));
|
|
2155
|
+
}
|
|
2156
|
+
async runRecordAction(context, model, action, id) {
|
|
2157
|
+
return success(await this.service.runAction(context, model, action, id));
|
|
2158
|
+
}
|
|
2159
|
+
async meta(context) {
|
|
2160
|
+
return success(await this.service.getMetadata(context));
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* `GET /admin/dashboard` - what the landing page shows.
|
|
2164
|
+
*
|
|
2165
|
+
* Declared before `:model` so the literal segment wins, as `meta` and
|
|
2166
|
+
* `actions` already are. The cost is the same and is documented with them: a
|
|
2167
|
+
* model named `dashboard` would be unreachable.
|
|
2168
|
+
*/
|
|
2169
|
+
async dashboard(context) {
|
|
2170
|
+
return success(await this.service.getDashboard(context));
|
|
2171
|
+
}
|
|
2172
|
+
async list(context, model, query) {
|
|
2173
|
+
const page = await this.service.list(context, model, query);
|
|
2174
|
+
return successPage(page.data, {
|
|
2175
|
+
total: page.total,
|
|
2176
|
+
page: page.page,
|
|
2177
|
+
perPage: page.perPage
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
async findOne(context, model, id) {
|
|
2181
|
+
return success(await this.service.findOne(context, model, id));
|
|
2182
|
+
}
|
|
2183
|
+
async create(context, model, body) {
|
|
2184
|
+
return success(await this.service.create(context, model, body));
|
|
2185
|
+
}
|
|
2186
|
+
async update(context, model, id, body) {
|
|
2187
|
+
return success(await this.service.update(context, model, id, body));
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Returns 200 with a `null` payload rather than 204, so every admin endpoint
|
|
2191
|
+
* answers with the same envelope and a client needs one response shape.
|
|
2192
|
+
*/
|
|
2193
|
+
async remove(context, model, id) {
|
|
2194
|
+
await this.service.delete(context, model, id);
|
|
2195
|
+
return success(null);
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* `DELETE /admin/:model` with `{ "ids": [...] }` - delete several records.
|
|
2199
|
+
*
|
|
2200
|
+
* One segment, so it cannot be confused with `/:model/:id`. The ids are in
|
|
2201
|
+
* the body rather than the query string because a selection of two hundred
|
|
2202
|
+
* would not survive a URL length limit, and a request that silently deletes
|
|
2203
|
+
* the first N of what was asked for is worse than one that fails.
|
|
2204
|
+
*
|
|
2205
|
+
* Answers 200 with both lists even when some records survived; see
|
|
2206
|
+
* `deleteMany` for why a partial result is not an error.
|
|
2207
|
+
*/
|
|
2208
|
+
async removeMany(context, model, body) {
|
|
2209
|
+
const ids = body?.ids;
|
|
2210
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string" && typeof id !== "number")) {
|
|
2211
|
+
throw new InvalidQueryError('Deleting records requires a body of the form { "ids": ["...", "..."] }.');
|
|
2212
|
+
}
|
|
2213
|
+
return success(await this.service.deleteMany(context, model, ids));
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* `GET /admin/:model/:id/:relation` - a page of related records.
|
|
2217
|
+
*
|
|
2218
|
+
* Three segments, so it cannot be confused with `/:model/:id`. The query
|
|
2219
|
+
* string is the ordinary list query and describes the records being returned,
|
|
2220
|
+
* not the record they hang off.
|
|
2221
|
+
*/
|
|
2222
|
+
async listRelated(context, model, id, relation, query) {
|
|
2223
|
+
const page = await this.service.listRelated(context, model, id, relation, query);
|
|
2224
|
+
return successPage(page.data, {
|
|
2225
|
+
total: page.total,
|
|
2226
|
+
page: page.page,
|
|
2227
|
+
perPage: page.perPage
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* `POST /admin/:model/:id/:relation` with `{ "id": "..." }` - link a record.
|
|
2232
|
+
*
|
|
2233
|
+
* The body carries only an id: this attaches something that already exists.
|
|
2234
|
+
* Creating a record and linking it in one request is a different operation
|
|
2235
|
+
* and is not this one.
|
|
2236
|
+
*/
|
|
2237
|
+
async attachRelated(context, model, id, relation, body) {
|
|
2238
|
+
const targetId = body?.id;
|
|
2239
|
+
if (typeof targetId !== "string" && typeof targetId !== "number") {
|
|
2240
|
+
throw new InvalidQueryError('Attaching a related record requires a body of the form { "id": "..." }.');
|
|
2241
|
+
}
|
|
2242
|
+
await this.service.attachRelated(context, model, id, relation, targetId);
|
|
2243
|
+
return success(null);
|
|
2244
|
+
}
|
|
2245
|
+
/** `DELETE /admin/:model/:id/:relation/:targetId` - unlink, without deleting. */
|
|
2246
|
+
async detachRelated(context, model, id, relation, targetId) {
|
|
2247
|
+
await this.service.detachRelated(context, model, id, relation, targetId);
|
|
2248
|
+
return success(null);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
_ts_decorate4([
|
|
2252
|
+
(0, import_common7.Post)("actions/:model/:action"),
|
|
2253
|
+
_ts_param3(0, AdminContext()),
|
|
2254
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2255
|
+
_ts_param3(2, (0, import_common7.Param)("action")),
|
|
2256
|
+
_ts_metadata3("design:type", Function),
|
|
2257
|
+
_ts_metadata3("design:paramtypes", [
|
|
2258
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2259
|
+
String,
|
|
2260
|
+
String
|
|
2261
|
+
]),
|
|
2262
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2263
|
+
], AdminController.prototype, "runListAction", null);
|
|
2264
|
+
_ts_decorate4([
|
|
2265
|
+
(0, import_common7.Post)("actions/:model/:action/:id"),
|
|
2266
|
+
_ts_param3(0, AdminContext()),
|
|
2267
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2268
|
+
_ts_param3(2, (0, import_common7.Param)("action")),
|
|
2269
|
+
_ts_param3(3, (0, import_common7.Param)("id")),
|
|
2270
|
+
_ts_metadata3("design:type", Function),
|
|
2271
|
+
_ts_metadata3("design:paramtypes", [
|
|
2272
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2273
|
+
String,
|
|
2274
|
+
String,
|
|
2275
|
+
String
|
|
2276
|
+
]),
|
|
2277
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2278
|
+
], AdminController.prototype, "runRecordAction", null);
|
|
2279
|
+
_ts_decorate4([
|
|
2280
|
+
(0, import_common7.Get)("meta"),
|
|
2281
|
+
_ts_param3(0, AdminContext()),
|
|
2282
|
+
_ts_metadata3("design:type", Function),
|
|
2283
|
+
_ts_metadata3("design:paramtypes", [
|
|
2284
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext
|
|
2285
|
+
]),
|
|
2286
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2287
|
+
], AdminController.prototype, "meta", null);
|
|
2288
|
+
_ts_decorate4([
|
|
2289
|
+
(0, import_common7.Get)("dashboard"),
|
|
2290
|
+
_ts_param3(0, AdminContext()),
|
|
2291
|
+
_ts_metadata3("design:type", Function),
|
|
2292
|
+
_ts_metadata3("design:paramtypes", [
|
|
2293
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext
|
|
2294
|
+
]),
|
|
2295
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2296
|
+
], AdminController.prototype, "dashboard", null);
|
|
2297
|
+
_ts_decorate4([
|
|
2298
|
+
(0, import_common7.Get)(":model"),
|
|
2299
|
+
_ts_param3(0, AdminContext()),
|
|
2300
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2301
|
+
_ts_param3(2, (0, import_common7.Query)()),
|
|
2302
|
+
_ts_metadata3("design:type", Function),
|
|
2303
|
+
_ts_metadata3("design:paramtypes", [
|
|
2304
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2305
|
+
String,
|
|
2306
|
+
typeof RawQuery === "undefined" ? Object : RawQuery
|
|
2307
|
+
]),
|
|
2308
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2309
|
+
], AdminController.prototype, "list", null);
|
|
2310
|
+
_ts_decorate4([
|
|
2311
|
+
(0, import_common7.Get)(":model/:id"),
|
|
2312
|
+
_ts_param3(0, AdminContext()),
|
|
2313
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2314
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2315
|
+
_ts_metadata3("design:type", Function),
|
|
2316
|
+
_ts_metadata3("design:paramtypes", [
|
|
2317
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2318
|
+
String,
|
|
2319
|
+
String
|
|
2320
|
+
]),
|
|
2321
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2322
|
+
], AdminController.prototype, "findOne", null);
|
|
2323
|
+
_ts_decorate4([
|
|
2324
|
+
(0, import_common7.Post)(":model"),
|
|
2325
|
+
_ts_param3(0, AdminContext()),
|
|
2326
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2327
|
+
_ts_param3(2, (0, import_common7.Body)()),
|
|
2328
|
+
_ts_metadata3("design:type", Function),
|
|
2329
|
+
_ts_metadata3("design:paramtypes", [
|
|
2330
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2331
|
+
String,
|
|
2332
|
+
typeof RecordData === "undefined" ? Object : RecordData
|
|
2333
|
+
]),
|
|
2334
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2335
|
+
], AdminController.prototype, "create", null);
|
|
2336
|
+
_ts_decorate4([
|
|
2337
|
+
(0, import_common7.Patch)(":model/:id"),
|
|
2338
|
+
_ts_param3(0, AdminContext()),
|
|
2339
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2340
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2341
|
+
_ts_param3(3, (0, import_common7.Body)()),
|
|
2342
|
+
_ts_metadata3("design:type", Function),
|
|
2343
|
+
_ts_metadata3("design:paramtypes", [
|
|
2344
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2345
|
+
String,
|
|
2346
|
+
String,
|
|
2347
|
+
typeof RecordData === "undefined" ? Object : RecordData
|
|
2348
|
+
]),
|
|
2349
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2350
|
+
], AdminController.prototype, "update", null);
|
|
2351
|
+
_ts_decorate4([
|
|
2352
|
+
(0, import_common7.Delete)(":model/:id"),
|
|
2353
|
+
_ts_param3(0, AdminContext()),
|
|
2354
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2355
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2356
|
+
_ts_metadata3("design:type", Function),
|
|
2357
|
+
_ts_metadata3("design:paramtypes", [
|
|
2358
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2359
|
+
String,
|
|
2360
|
+
String
|
|
2361
|
+
]),
|
|
2362
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2363
|
+
], AdminController.prototype, "remove", null);
|
|
2364
|
+
_ts_decorate4([
|
|
2365
|
+
(0, import_common7.Delete)(":model"),
|
|
2366
|
+
_ts_param3(0, AdminContext()),
|
|
2367
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2368
|
+
_ts_param3(2, (0, import_common7.Body)()),
|
|
2369
|
+
_ts_metadata3("design:type", Function),
|
|
2370
|
+
_ts_metadata3("design:paramtypes", [
|
|
2371
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2372
|
+
String,
|
|
2373
|
+
Object
|
|
2374
|
+
]),
|
|
2375
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2376
|
+
], AdminController.prototype, "removeMany", null);
|
|
2377
|
+
_ts_decorate4([
|
|
2378
|
+
(0, import_common7.Get)(":model/:id/:relation"),
|
|
2379
|
+
_ts_param3(0, AdminContext()),
|
|
2380
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2381
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2382
|
+
_ts_param3(3, (0, import_common7.Param)("relation")),
|
|
2383
|
+
_ts_param3(4, (0, import_common7.Query)()),
|
|
2384
|
+
_ts_metadata3("design:type", Function),
|
|
2385
|
+
_ts_metadata3("design:paramtypes", [
|
|
2386
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2387
|
+
String,
|
|
2388
|
+
String,
|
|
2389
|
+
String,
|
|
2390
|
+
typeof RawQuery === "undefined" ? Object : RawQuery
|
|
2391
|
+
]),
|
|
2392
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2393
|
+
], AdminController.prototype, "listRelated", null);
|
|
2394
|
+
_ts_decorate4([
|
|
2395
|
+
(0, import_common7.Post)(":model/:id/:relation"),
|
|
2396
|
+
_ts_param3(0, AdminContext()),
|
|
2397
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2398
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2399
|
+
_ts_param3(3, (0, import_common7.Param)("relation")),
|
|
2400
|
+
_ts_param3(4, (0, import_common7.Body)()),
|
|
2401
|
+
_ts_metadata3("design:type", Function),
|
|
2402
|
+
_ts_metadata3("design:paramtypes", [
|
|
2403
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2404
|
+
String,
|
|
2405
|
+
String,
|
|
2406
|
+
String,
|
|
2407
|
+
Object
|
|
2408
|
+
]),
|
|
2409
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2410
|
+
], AdminController.prototype, "attachRelated", null);
|
|
2411
|
+
_ts_decorate4([
|
|
2412
|
+
(0, import_common7.Delete)(":model/:id/:relation/:targetId"),
|
|
2413
|
+
_ts_param3(0, AdminContext()),
|
|
2414
|
+
_ts_param3(1, (0, import_common7.Param)("model")),
|
|
2415
|
+
_ts_param3(2, (0, import_common7.Param)("id")),
|
|
2416
|
+
_ts_param3(3, (0, import_common7.Param)("relation")),
|
|
2417
|
+
_ts_param3(4, (0, import_common7.Param)("targetId")),
|
|
2418
|
+
_ts_metadata3("design:type", Function),
|
|
2419
|
+
_ts_metadata3("design:paramtypes", [
|
|
2420
|
+
typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
|
|
2421
|
+
String,
|
|
2422
|
+
String,
|
|
2423
|
+
String,
|
|
2424
|
+
String
|
|
2425
|
+
]),
|
|
2426
|
+
_ts_metadata3("design:returntype", Promise)
|
|
2427
|
+
], AdminController.prototype, "detachRelated", null);
|
|
2428
|
+
AdminController = _ts_decorate4([
|
|
2429
|
+
(0, import_common7.Controller)(),
|
|
2430
|
+
(0, import_common7.UseGuards)(AdminAuthGuard),
|
|
2431
|
+
(0, import_common7.UseFilters)(AdminExceptionFilter),
|
|
2432
|
+
_ts_metadata3("design:type", Function),
|
|
2433
|
+
_ts_metadata3("design:paramtypes", [
|
|
2434
|
+
typeof AdminService === "undefined" ? Object : AdminService
|
|
2435
|
+
])
|
|
2436
|
+
], AdminController);
|
|
2437
|
+
|
|
2438
|
+
// src/auth/controller.ts
|
|
2439
|
+
var import_common8 = require("@nestjs/common");
|
|
2440
|
+
function _ts_decorate5(decorators, target, key, desc) {
|
|
2441
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2442
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
2443
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
2444
|
+
} else {
|
|
2445
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
2446
|
+
if (d = decorators[i]) {
|
|
2447
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2452
|
+
}
|
|
2453
|
+
__name(_ts_decorate5, "_ts_decorate");
|
|
2454
|
+
function _ts_metadata4(metadataKey, metadataValue) {
|
|
2455
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
2456
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
__name(_ts_metadata4, "_ts_metadata");
|
|
2460
|
+
function _ts_param4(paramIndex, decorator) {
|
|
2461
|
+
return function(target, key) {
|
|
2462
|
+
decorator(target, key, paramIndex);
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
__name(_ts_param4, "_ts_param");
|
|
2466
|
+
var AdminAuthController = class {
|
|
2467
|
+
static {
|
|
2468
|
+
__name(this, "AdminAuthController");
|
|
2469
|
+
}
|
|
2470
|
+
auth;
|
|
2471
|
+
constructor(auth) {
|
|
2472
|
+
this.auth = auth;
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* `GET /admin/auth/session` - who is signed in, if anyone.
|
|
2476
|
+
*
|
|
2477
|
+
* Answers `200` with `account: null` rather than `401` for an absent
|
|
2478
|
+
* session. The interface asks this before it has any reason to think anybody
|
|
2479
|
+
* is signed in, and an error is the wrong shape for "no, and that is fine" -
|
|
2480
|
+
* it would put a failure in the console on every visit to the login page.
|
|
2481
|
+
*/
|
|
2482
|
+
async session(request) {
|
|
2483
|
+
const runtime = this.runtime();
|
|
2484
|
+
const token = cookieFrom(request?.headers?.cookie, runtime.cookieName);
|
|
2485
|
+
const id = token === void 0 ? void 0 : readSession(token, runtime.secret);
|
|
2486
|
+
if (id === void 0) return success({
|
|
2487
|
+
account: null
|
|
2488
|
+
});
|
|
2489
|
+
const account = await runtime.store.findById(id);
|
|
2490
|
+
return success({
|
|
2491
|
+
account: !account || account.disabled === true ? null : summarise(account)
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* `POST /admin/auth/login` with `{ email, password }`.
|
|
2496
|
+
*
|
|
2497
|
+
* `200` and a cookie, or `401` and nothing. There is exactly one failure
|
|
2498
|
+
* message: an unknown address, a wrong password, a disabled account and a
|
|
2499
|
+
* locked-out one are indistinguishable from outside. Telling them apart is
|
|
2500
|
+
* convenient for the person signing in perhaps twice a year, and a list of
|
|
2501
|
+
* which addresses are registered for everyone else.
|
|
2502
|
+
*/
|
|
2503
|
+
async login(request, response, body) {
|
|
2504
|
+
const runtime = this.runtime();
|
|
2505
|
+
assertSameOrigin(request);
|
|
2506
|
+
if (typeof body?.email !== "string" || typeof body?.password !== "string") {
|
|
2507
|
+
throw new InvalidQueryError("Signing in requires an email and a password.");
|
|
2508
|
+
}
|
|
2509
|
+
const account = await runtime.signIn(body.email, body.password, attemptKey(request, body.email));
|
|
2510
|
+
if (!account) {
|
|
2511
|
+
throw new UnauthorizedError("Those details do not match an account.");
|
|
2512
|
+
}
|
|
2513
|
+
setSessionCookie(response, signSession(account.id, runtime.secret, runtime.maxAge), runtime, request);
|
|
2514
|
+
return success({
|
|
2515
|
+
account: summarise(account)
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* `POST /admin/auth/logout`.
|
|
2520
|
+
*
|
|
2521
|
+
* A POST rather than a GET, so a link or an image somewhere else cannot sign
|
|
2522
|
+
* someone out by being loaded. Always `200`: signing out when you were not
|
|
2523
|
+
* signed in is not a failure, it is the state you asked for.
|
|
2524
|
+
*/
|
|
2525
|
+
logout(request, response) {
|
|
2526
|
+
const runtime = this.runtime();
|
|
2527
|
+
assertSameOrigin(request);
|
|
2528
|
+
clearSessionCookie(response, runtime, request);
|
|
2529
|
+
return success({
|
|
2530
|
+
account: null
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* The built-in auth's runtime, or a 404.
|
|
2535
|
+
*
|
|
2536
|
+
* Not a 500: an application using its own `AdminAuth` has no login routes,
|
|
2537
|
+
* and "this endpoint does not exist here" is exactly true.
|
|
2538
|
+
*/
|
|
2539
|
+
runtime() {
|
|
2540
|
+
const runtime = builtInRuntimeOf(this.auth);
|
|
2541
|
+
if (!runtime) {
|
|
2542
|
+
throw new import_common8.NotFoundException("This admin is not using the built-in authentication, so it has no login routes.");
|
|
2543
|
+
}
|
|
2544
|
+
return runtime;
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
_ts_decorate5([
|
|
2548
|
+
(0, import_common8.Get)("session"),
|
|
2549
|
+
_ts_param4(0, (0, import_common8.Req)()),
|
|
2550
|
+
_ts_metadata4("design:type", Function),
|
|
2551
|
+
_ts_metadata4("design:paramtypes", [
|
|
2552
|
+
typeof RawRequest === "undefined" ? Object : RawRequest
|
|
2553
|
+
]),
|
|
2554
|
+
_ts_metadata4("design:returntype", Promise)
|
|
2555
|
+
], AdminAuthController.prototype, "session", null);
|
|
2556
|
+
_ts_decorate5([
|
|
2557
|
+
(0, import_common8.Post)("login"),
|
|
2558
|
+
(0, import_common8.HttpCode)(200),
|
|
2559
|
+
_ts_param4(0, (0, import_common8.Req)()),
|
|
2560
|
+
_ts_param4(1, (0, import_common8.Res)({
|
|
2561
|
+
passthrough: true
|
|
2562
|
+
})),
|
|
2563
|
+
_ts_param4(2, (0, import_common8.Body)()),
|
|
2564
|
+
_ts_metadata4("design:type", Function),
|
|
2565
|
+
_ts_metadata4("design:paramtypes", [
|
|
2566
|
+
typeof RawRequest === "undefined" ? Object : RawRequest,
|
|
2567
|
+
Object,
|
|
2568
|
+
Object
|
|
2569
|
+
]),
|
|
2570
|
+
_ts_metadata4("design:returntype", Promise)
|
|
2571
|
+
], AdminAuthController.prototype, "login", null);
|
|
2572
|
+
_ts_decorate5([
|
|
2573
|
+
(0, import_common8.Post)("logout"),
|
|
2574
|
+
(0, import_common8.HttpCode)(200),
|
|
2575
|
+
_ts_param4(0, (0, import_common8.Req)()),
|
|
2576
|
+
_ts_param4(1, (0, import_common8.Res)({
|
|
2577
|
+
passthrough: true
|
|
2578
|
+
})),
|
|
2579
|
+
_ts_metadata4("design:type", Function),
|
|
2580
|
+
_ts_metadata4("design:paramtypes", [
|
|
2581
|
+
typeof RawRequest === "undefined" ? Object : RawRequest,
|
|
2582
|
+
Object
|
|
2583
|
+
]),
|
|
2584
|
+
_ts_metadata4("design:returntype", typeof SuccessResponse === "undefined" ? Object : SuccessResponse)
|
|
2585
|
+
], AdminAuthController.prototype, "logout", null);
|
|
2586
|
+
AdminAuthController = _ts_decorate5([
|
|
2587
|
+
(0, import_common8.Controller)("auth"),
|
|
2588
|
+
(0, import_common8.UseFilters)(AdminExceptionFilter),
|
|
2589
|
+
_ts_param4(0, (0, import_common8.Inject)(ADMIN_AUTH)),
|
|
2590
|
+
_ts_metadata4("design:type", Function),
|
|
2591
|
+
_ts_metadata4("design:paramtypes", [
|
|
2592
|
+
typeof AdminAuth === "undefined" ? Object : AdminAuth
|
|
2593
|
+
])
|
|
2594
|
+
], AdminAuthController);
|
|
2595
|
+
function assertSameOrigin(request) {
|
|
2596
|
+
const origin = header(request, "origin");
|
|
2597
|
+
if (origin === void 0) return;
|
|
2598
|
+
const host = header(request, "host");
|
|
2599
|
+
if (host === void 0) return;
|
|
2600
|
+
let sent;
|
|
2601
|
+
try {
|
|
2602
|
+
sent = new URL(origin).host;
|
|
2603
|
+
} catch {
|
|
2604
|
+
throw new UnauthorizedError("This request did not come from the admin.");
|
|
2605
|
+
}
|
|
2606
|
+
if (sent !== host) {
|
|
2607
|
+
throw new UnauthorizedError("This request did not come from the admin.");
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
__name(assertSameOrigin, "assertSameOrigin");
|
|
2611
|
+
function header(request, name) {
|
|
2612
|
+
const value = request?.headers?.[name];
|
|
2613
|
+
return typeof value === "string" ? value : void 0;
|
|
2614
|
+
}
|
|
2615
|
+
__name(header, "header");
|
|
2616
|
+
|
|
2617
|
+
// src/auth/contract.ts
|
|
2618
|
+
var import_common9 = require("@nestjs/common");
|
|
2619
|
+
var logger3 = new import_common9.Logger("NestAdmin");
|
|
2620
|
+
function unsafeAllowAllRequests() {
|
|
2621
|
+
const auth = {
|
|
2622
|
+
authorize() {
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
unsafeInstances.add(auth);
|
|
2626
|
+
return auth;
|
|
2627
|
+
}
|
|
2628
|
+
__name(unsafeAllowAllRequests, "unsafeAllowAllRequests");
|
|
2629
|
+
var unsafeInstances = /* @__PURE__ */ new WeakSet();
|
|
2630
|
+
function warnIfUnsafe(auth) {
|
|
2631
|
+
if (unsafeInstances.has(auth)) {
|
|
2632
|
+
logger3.warn("AdminModule is running with unsafeAllowAllRequests(): every admin route, including /admin/meta, is public. Do not deploy this.");
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
__name(warnIfUnsafe, "warnIfUnsafe");
|
|
2636
|
+
|
|
2637
|
+
// src/auth/resource.ts
|
|
2638
|
+
function allowAllResources() {
|
|
2639
|
+
return {
|
|
2640
|
+
authorize() {
|
|
2641
|
+
return true;
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
__name(allowAllResources, "allowAllResources");
|
|
2646
|
+
|
|
2647
|
+
// src/mount-path.ts
|
|
2648
|
+
var DEFAULT_MOUNT_PATH = "/admin";
|
|
2649
|
+
function normaliseMountPath(path) {
|
|
2650
|
+
if (path === void 0) return DEFAULT_MOUNT_PATH;
|
|
2651
|
+
if (typeof path !== "string") {
|
|
2652
|
+
throw new TypeError(`AdminModule \`path\` must be a string, received ${typeof path}.`);
|
|
2653
|
+
}
|
|
2654
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
2655
|
+
if (segments.length === 0) {
|
|
2656
|
+
throw new Error('AdminModule `path` cannot be empty or "/". The admin routes end in `:model`, so mounting them at the root would capture every unmatched request in the application. Choose a path such as "/admin".');
|
|
2657
|
+
}
|
|
2658
|
+
for (const segment of segments) {
|
|
2659
|
+
if (!/^[A-Za-z0-9._~-]+$/.test(segment)) {
|
|
2660
|
+
throw new Error(`AdminModule \`path\` segment "${segment}" is not a plain path segment. Use letters, digits, and any of . _ ~ - : the admin builds both URLs and HTML from this value, so it cannot contain patterns or markup.`);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return `/${segments.join("/")}`;
|
|
2664
|
+
}
|
|
2665
|
+
__name(normaliseMountPath, "normaliseMountPath");
|
|
2666
|
+
|
|
2667
|
+
// src/ui/assets.ts
|
|
2668
|
+
var import_node_fs = require("fs");
|
|
2669
|
+
var import_node_path = require("path");
|
|
2670
|
+
var import_node_url = require("url");
|
|
2671
|
+
|
|
2672
|
+
// src/ui/colour.ts
|
|
2673
|
+
var READABLE = 4.5;
|
|
2674
|
+
var VISIBLE = 3;
|
|
2675
|
+
function channels(hex) {
|
|
2676
|
+
const value = hex.length === 4 ? hex.slice(1).split("").map((part) => part + part).join("") : hex.slice(1);
|
|
2677
|
+
return [
|
|
2678
|
+
Number.parseInt(value.slice(0, 2), 16) / 255,
|
|
2679
|
+
Number.parseInt(value.slice(2, 4), 16) / 255,
|
|
2680
|
+
Number.parseInt(value.slice(4, 6), 16) / 255
|
|
2681
|
+
];
|
|
2682
|
+
}
|
|
2683
|
+
__name(channels, "channels");
|
|
2684
|
+
function toHex([r, g, b]) {
|
|
2685
|
+
const part = /* @__PURE__ */ __name((value) => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, "0"), "part");
|
|
2686
|
+
return `#${part(r)}${part(g)}${part(b)}`;
|
|
2687
|
+
}
|
|
2688
|
+
__name(toHex, "toHex");
|
|
2689
|
+
function luminance(rgb) {
|
|
2690
|
+
const [r, g, b] = rgb.map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
|
|
2691
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2692
|
+
}
|
|
2693
|
+
__name(luminance, "luminance");
|
|
2694
|
+
function contrast(a, b) {
|
|
2695
|
+
const [lighter, darker] = [
|
|
2696
|
+
luminance(a),
|
|
2697
|
+
luminance(b)
|
|
2698
|
+
].sort((x, y) => y - x);
|
|
2699
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
2700
|
+
}
|
|
2701
|
+
__name(contrast, "contrast");
|
|
2702
|
+
var LIGHT_INK = [
|
|
2703
|
+
0.985,
|
|
2704
|
+
0.985,
|
|
2705
|
+
0.99
|
|
2706
|
+
];
|
|
2707
|
+
var DARK_INK = [
|
|
2708
|
+
0.09,
|
|
2709
|
+
0.1,
|
|
2710
|
+
0.12
|
|
2711
|
+
];
|
|
2712
|
+
var LIGHT_PAGE = [
|
|
2713
|
+
0.976,
|
|
2714
|
+
0.98,
|
|
2715
|
+
0.984
|
|
2716
|
+
];
|
|
2717
|
+
var DARK_PAGE = [
|
|
2718
|
+
0.09,
|
|
2719
|
+
0.1,
|
|
2720
|
+
0.12
|
|
2721
|
+
];
|
|
2722
|
+
function readableInk(hex) {
|
|
2723
|
+
const brand = channels(hex);
|
|
2724
|
+
return contrast(brand, LIGHT_INK) >= contrast(brand, DARK_INK) ? toHex(LIGHT_INK) : toHex(DARK_INK);
|
|
2725
|
+
}
|
|
2726
|
+
__name(readableInk, "readableInk");
|
|
2727
|
+
function mix(rgb, towards, amount) {
|
|
2728
|
+
return rgb.map((channel) => channel + (towards - channel) * amount);
|
|
2729
|
+
}
|
|
2730
|
+
__name(mix, "mix");
|
|
2731
|
+
function visibleOn(hex, page, role = "text") {
|
|
2732
|
+
const background = page === "dark" ? DARK_PAGE : LIGHT_PAGE;
|
|
2733
|
+
const towards = page === "dark" ? 1 : 0;
|
|
2734
|
+
const floor = role === "fill" ? VISIBLE : READABLE;
|
|
2735
|
+
let colour = channels(hex);
|
|
2736
|
+
for (let step = 0; step < 16; step++) {
|
|
2737
|
+
if (contrast(colour, background) >= floor) break;
|
|
2738
|
+
colour = mix(colour, towards, 0.05);
|
|
2739
|
+
}
|
|
2740
|
+
return toHex(colour);
|
|
2741
|
+
}
|
|
2742
|
+
__name(visibleOn, "visibleOn");
|
|
2743
|
+
|
|
2744
|
+
// src/ui/theme.ts
|
|
2745
|
+
var HEX_COLOUR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
2746
|
+
var SAFE_TEXT = /^[^<>&"'`\\]{1,64}$/;
|
|
2747
|
+
var SAFE_URL = /^(?:https?:\/\/[^\s<>"'`\\]+|data:image\/[a-z+]+;base64,[A-Za-z0-9+/=]+)$/;
|
|
2748
|
+
var APPEARANCES = /* @__PURE__ */ new Set([
|
|
2749
|
+
"system",
|
|
2750
|
+
"light",
|
|
2751
|
+
"dark"
|
|
2752
|
+
]);
|
|
2753
|
+
var THEME_KEYS = /* @__PURE__ */ new Set([
|
|
2754
|
+
"brandColor",
|
|
2755
|
+
"title",
|
|
2756
|
+
"logoUrl",
|
|
2757
|
+
"appearance"
|
|
2758
|
+
]);
|
|
2759
|
+
function assertUsableTheme(theme) {
|
|
2760
|
+
if (!theme) return;
|
|
2761
|
+
if (theme.brandColor !== void 0 && !HEX_COLOUR.test(theme.brandColor)) {
|
|
2762
|
+
throw new Error(`AdminModule \`theme.brandColor\` must be a hex colour such as "#0b6e6e", received ${JSON.stringify(theme.brandColor)}.`);
|
|
2763
|
+
}
|
|
2764
|
+
if (theme.title !== void 0 && !SAFE_TEXT.test(theme.title)) {
|
|
2765
|
+
throw new Error(`AdminModule \`theme.title\` must be plain text of at most 64 characters, without < > & " ' \` or backslashes. It is written into the served page.`);
|
|
2766
|
+
}
|
|
2767
|
+
if (theme.logoUrl !== void 0 && !SAFE_URL.test(theme.logoUrl)) {
|
|
2768
|
+
throw new Error(`AdminModule \`theme.logoUrl\` must be an http(s) URL or a data:image URI, received ${JSON.stringify(theme.logoUrl)}.`);
|
|
2769
|
+
}
|
|
2770
|
+
if (theme.appearance !== void 0 && !APPEARANCES.has(theme.appearance)) {
|
|
2771
|
+
throw new Error(`AdminModule \`theme.appearance\` must be "system", "light" or "dark", received ${JSON.stringify(theme.appearance)}.`);
|
|
2772
|
+
}
|
|
2773
|
+
const unknown = Object.keys(theme).filter((key) => !THEME_KEYS.has(key));
|
|
2774
|
+
if (unknown.length > 0) {
|
|
2775
|
+
throw new Error(`AdminModule \`theme\` has no option${unknown.length === 1 ? "" : "s"} called ${unknown.join(", ")}. Known options: ${[
|
|
2776
|
+
...THEME_KEYS
|
|
2777
|
+
].join(", ")}.`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
__name(assertUsableTheme, "assertUsableTheme");
|
|
2781
|
+
function renderTheme(theme) {
|
|
2782
|
+
if (!theme || !theme.brandColor && !theme.title && !theme.logoUrl && !theme.appearance) {
|
|
2783
|
+
return "";
|
|
2784
|
+
}
|
|
2785
|
+
const style = theme.brandColor ? `<style>${brandRules(theme.brandColor)}</style>` : "";
|
|
2786
|
+
const globals = JSON.stringify({
|
|
2787
|
+
...theme.title !== void 0 ? {
|
|
2788
|
+
title: theme.title
|
|
2789
|
+
} : {},
|
|
2790
|
+
...theme.logoUrl !== void 0 ? {
|
|
2791
|
+
logoUrl: theme.logoUrl
|
|
2792
|
+
} : {},
|
|
2793
|
+
...theme.appearance !== void 0 ? {
|
|
2794
|
+
appearance: theme.appearance
|
|
2795
|
+
} : {}
|
|
2796
|
+
});
|
|
2797
|
+
return `${style}<script>window.__NEST_ADMIN_THEME__ = ${globals}</script>`;
|
|
2798
|
+
}
|
|
2799
|
+
__name(renderTheme, "renderTheme");
|
|
2800
|
+
function brandRules(brand) {
|
|
2801
|
+
const rules = /* @__PURE__ */ __name((page) => {
|
|
2802
|
+
const fill = visibleOn(brand, page, "fill");
|
|
2803
|
+
const text = visibleOn(brand, page, "text");
|
|
2804
|
+
return `--primary:${fill};--primary-foreground:${readableInk(fill)};--link:${text}`;
|
|
2805
|
+
}, "rules");
|
|
2806
|
+
return `:root{${rules("light")}}.dark{${rules("dark")}}`;
|
|
2807
|
+
}
|
|
2808
|
+
__name(brandRules, "brandRules");
|
|
2809
|
+
|
|
2810
|
+
// src/ui/assets.ts
|
|
2811
|
+
function moduleDirectory() {
|
|
2812
|
+
return (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
2813
|
+
}
|
|
2814
|
+
__name(moduleDirectory, "moduleDirectory");
|
|
2815
|
+
function uiRoot() {
|
|
2816
|
+
return (0, import_node_path.join)(moduleDirectory(), "admin-ui");
|
|
2817
|
+
}
|
|
2818
|
+
__name(uiRoot, "uiRoot");
|
|
2819
|
+
function uiAvailable(root = uiRoot()) {
|
|
2820
|
+
return (0, import_node_fs.existsSync)((0, import_node_path.join)(root, "index.html"));
|
|
2821
|
+
}
|
|
2822
|
+
__name(uiAvailable, "uiAvailable");
|
|
2823
|
+
var CONTENT_TYPES = {
|
|
2824
|
+
".html": "text/html; charset=utf-8",
|
|
2825
|
+
".js": "text/javascript; charset=utf-8",
|
|
2826
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
2827
|
+
".css": "text/css; charset=utf-8",
|
|
2828
|
+
".map": "application/json; charset=utf-8",
|
|
2829
|
+
".json": "application/json; charset=utf-8",
|
|
2830
|
+
".svg": "image/svg+xml",
|
|
2831
|
+
".png": "image/png",
|
|
2832
|
+
".jpg": "image/jpeg",
|
|
2833
|
+
".jpeg": "image/jpeg",
|
|
2834
|
+
".gif": "image/gif",
|
|
2835
|
+
".ico": "image/x-icon",
|
|
2836
|
+
".webp": "image/webp",
|
|
2837
|
+
".woff": "font/woff",
|
|
2838
|
+
".woff2": "font/woff2",
|
|
2839
|
+
".ttf": "font/ttf"
|
|
2840
|
+
};
|
|
2841
|
+
function contentTypeFor(fileName) {
|
|
2842
|
+
const dot = fileName.lastIndexOf(".");
|
|
2843
|
+
const extension = dot === -1 ? "" : fileName.slice(dot).toLowerCase();
|
|
2844
|
+
return CONTENT_TYPES[extension] ?? "application/octet-stream";
|
|
2845
|
+
}
|
|
2846
|
+
__name(contentTypeFor, "contentTypeFor");
|
|
2847
|
+
function readAsset(fileName, root = uiRoot()) {
|
|
2848
|
+
if (!/^[\w.-]+$/.test(fileName) || fileName.includes("..")) return void 0;
|
|
2849
|
+
const assetsDirectory = (0, import_node_path.join)(root, "assets");
|
|
2850
|
+
const candidate = (0, import_node_path.resolve)(assetsDirectory, fileName);
|
|
2851
|
+
if (!candidate.startsWith(assetsDirectory + import_node_path.sep)) return void 0;
|
|
2852
|
+
if (!(0, import_node_fs.existsSync)(candidate) || !(0, import_node_fs.statSync)(candidate).isFile()) return void 0;
|
|
2853
|
+
return (0, import_node_fs.readFileSync)(candidate);
|
|
2854
|
+
}
|
|
2855
|
+
__name(readAsset, "readAsset");
|
|
2856
|
+
function readIndexHtml(root = uiRoot()) {
|
|
2857
|
+
const indexPath = (0, import_node_path.join)(root, "index.html");
|
|
2858
|
+
return (0, import_node_fs.existsSync)(indexPath) ? (0, import_node_fs.readFileSync)(indexPath) : void 0;
|
|
2859
|
+
}
|
|
2860
|
+
__name(readIndexHtml, "readIndexHtml");
|
|
2861
|
+
var UI_BASE_PLACEHOLDER = "/__nest-admin-base__";
|
|
2862
|
+
function renderShell(mountPath, root = uiRoot(), theme) {
|
|
2863
|
+
const shell = readIndexHtml(root);
|
|
2864
|
+
if (!shell) return void 0;
|
|
2865
|
+
const injected = ` <script>window.__NEST_ADMIN_BASE__ = "${mountPath}"</script>
|
|
2866
|
+
${renderTheme(theme)}
|
|
2867
|
+
</head>`;
|
|
2868
|
+
let html = shell.toString("utf8").split(`${UI_BASE_PLACEHOLDER}/`).join(`${mountPath}/`).replace("</head>", injected);
|
|
2869
|
+
if (theme?.title !== void 0) {
|
|
2870
|
+
html = html.replace(/<title>[^<]*<\/title>/, `<title>${theme.title}</title>`);
|
|
2871
|
+
}
|
|
2872
|
+
return Buffer.from(html, "utf8");
|
|
2873
|
+
}
|
|
2874
|
+
__name(renderShell, "renderShell");
|
|
2875
|
+
|
|
2876
|
+
// src/ui/controller.ts
|
|
2877
|
+
var import_common10 = require("@nestjs/common");
|
|
2878
|
+
function _ts_decorate6(decorators, target, key, desc) {
|
|
2879
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2880
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
2881
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
2882
|
+
} else {
|
|
2883
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
2884
|
+
if (d = decorators[i]) {
|
|
2885
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2890
|
+
}
|
|
2891
|
+
__name(_ts_decorate6, "_ts_decorate");
|
|
2892
|
+
function _ts_metadata5(metadataKey, metadataValue) {
|
|
2893
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
2894
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
__name(_ts_metadata5, "_ts_metadata");
|
|
2898
|
+
function _ts_param5(paramIndex, decorator) {
|
|
2899
|
+
return function(target, key) {
|
|
2900
|
+
decorator(target, key, paramIndex);
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
__name(_ts_param5, "_ts_param");
|
|
2904
|
+
var AdminUiController = class {
|
|
2905
|
+
static {
|
|
2906
|
+
__name(this, "AdminUiController");
|
|
2907
|
+
}
|
|
2908
|
+
root;
|
|
2909
|
+
mountPath;
|
|
2910
|
+
theme;
|
|
2911
|
+
/**
|
|
2912
|
+
* The rendered shell, built on first use.
|
|
2913
|
+
*
|
|
2914
|
+
* It depends only on the bundled file and the mount path, and neither changes
|
|
2915
|
+
* while the application runs. Held on the instance rather than in a
|
|
2916
|
+
* module-level cache: the package ships two bundles that each inline their
|
|
2917
|
+
* own copy of a module, so module-level state is not shared between them.
|
|
2918
|
+
*/
|
|
2919
|
+
shell;
|
|
2920
|
+
constructor(root, mountPath, theme) {
|
|
2921
|
+
this.root = root;
|
|
2922
|
+
this.mountPath = mountPath;
|
|
2923
|
+
this.theme = theme;
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* `GET /admin` - the SPA shell.
|
|
2927
|
+
*
|
|
2928
|
+
* `no-cache` rather than a long max-age: the HTML names hashed asset files,
|
|
2929
|
+
* so a cached shell would keep pointing at a previous deployment's bundle.
|
|
2930
|
+
*/
|
|
2931
|
+
index() {
|
|
2932
|
+
this.shell ??= renderShell(this.mountPath, this.root, this.theme);
|
|
2933
|
+
const html = this.shell;
|
|
2934
|
+
if (!html) {
|
|
2935
|
+
throw new import_common10.NotFoundException("The admin UI was not bundled with this package. This build is missing dist/admin-ui - see docs/publishing.md.");
|
|
2936
|
+
}
|
|
2937
|
+
return new import_common10.StreamableFile(html, {
|
|
2938
|
+
type: "text/html; charset=utf-8"
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* `GET /admin/assets/:file`
|
|
2943
|
+
*
|
|
2944
|
+
* A single path segment, so a nested path cannot be requested at all; the
|
|
2945
|
+
* reader in `assets.ts` re-checks the name and the resolved location anyway.
|
|
2946
|
+
*
|
|
2947
|
+
* Vite emits content-hashed filenames, so these are immutable and safe to
|
|
2948
|
+
* cache for a long time.
|
|
2949
|
+
*/
|
|
2950
|
+
asset(file) {
|
|
2951
|
+
const contents = readAsset(file, this.root);
|
|
2952
|
+
if (!contents) throw new import_common10.NotFoundException(`No admin UI asset named "${file}".`);
|
|
2953
|
+
return new import_common10.StreamableFile(contents, {
|
|
2954
|
+
type: contentTypeFor(file)
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
_ts_decorate6([
|
|
2959
|
+
(0, import_common10.Get)(),
|
|
2960
|
+
(0, import_common10.Header)("Cache-Control", "no-cache"),
|
|
2961
|
+
_ts_metadata5("design:type", Function),
|
|
2962
|
+
_ts_metadata5("design:paramtypes", []),
|
|
2963
|
+
_ts_metadata5("design:returntype", typeof import_common10.StreamableFile === "undefined" ? Object : import_common10.StreamableFile)
|
|
2964
|
+
], AdminUiController.prototype, "index", null);
|
|
2965
|
+
_ts_decorate6([
|
|
2966
|
+
(0, import_common10.Get)("assets/:file"),
|
|
2967
|
+
(0, import_common10.Header)("Cache-Control", "public, max-age=31536000, immutable"),
|
|
2968
|
+
_ts_param5(0, (0, import_common10.Param)("file")),
|
|
2969
|
+
_ts_metadata5("design:type", Function),
|
|
2970
|
+
_ts_metadata5("design:paramtypes", [
|
|
2971
|
+
String
|
|
2972
|
+
]),
|
|
2973
|
+
_ts_metadata5("design:returntype", typeof import_common10.StreamableFile === "undefined" ? Object : import_common10.StreamableFile)
|
|
2974
|
+
], AdminUiController.prototype, "asset", null);
|
|
2975
|
+
AdminUiController = _ts_decorate6([
|
|
2976
|
+
(0, import_common10.Controller)(),
|
|
2977
|
+
_ts_param5(0, (0, import_common10.Inject)(ADMIN_UI_ROOT)),
|
|
2978
|
+
_ts_param5(1, (0, import_common10.Inject)(ADMIN_MOUNT_PATH)),
|
|
2979
|
+
_ts_param5(2, (0, import_common10.Inject)(ADMIN_THEME)),
|
|
2980
|
+
_ts_metadata5("design:type", Function),
|
|
2981
|
+
_ts_metadata5("design:paramtypes", [
|
|
2982
|
+
String,
|
|
2983
|
+
String,
|
|
2984
|
+
Object
|
|
2985
|
+
])
|
|
2986
|
+
], AdminUiController);
|
|
2987
|
+
|
|
2988
|
+
// src/module.ts
|
|
2989
|
+
function _ts_decorate7(decorators, target, key, desc) {
|
|
2990
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2991
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
2992
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
2993
|
+
} else {
|
|
2994
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
2995
|
+
if (d = decorators[i]) {
|
|
2996
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3001
|
+
}
|
|
3002
|
+
__name(_ts_decorate7, "_ts_decorate");
|
|
3003
|
+
function assertUsableOptions(options, caller) {
|
|
3004
|
+
if (!options?.adapter) {
|
|
3005
|
+
throw new Error(`AdminModule.${caller}() requires an \`adapter\`. Construct one in your application, for example \`new PrismaAdapter({ client: prisma })\`.`);
|
|
3006
|
+
}
|
|
3007
|
+
if (!options.auth || typeof options.auth.authorize !== "function") {
|
|
3008
|
+
throw new Error(`AdminModule.${caller}() requires an \`auth\` implementation with an \`authorize(context)\` method. The admin API exposes every record and the whole schema, so it is never public by default. For local development only, pass \`unsafeAllowAllRequests()\`.`);
|
|
3009
|
+
}
|
|
3010
|
+
if (options.resourceAuth && typeof options.resourceAuth.authorize !== "function") {
|
|
3011
|
+
throw new Error(`AdminModule.${caller}() was given a \`resourceAuth\` without an \`authorize(resource)\` method. Omit it to allow every model, or supply an implementation.`);
|
|
3012
|
+
}
|
|
3013
|
+
warnIfUnsafe(options.auth);
|
|
3014
|
+
}
|
|
3015
|
+
__name(assertUsableOptions, "assertUsableOptions");
|
|
3016
|
+
function warnIfUiMissing(resolvedUiRoot, mountPath) {
|
|
3017
|
+
if (uiAvailable(resolvedUiRoot)) return;
|
|
3018
|
+
new import_common11.Logger("NestAdmin").warn(`The admin UI was not found in this build; ${mountPath} will return 404. The API under ${mountPath} is unaffected.`);
|
|
3019
|
+
}
|
|
3020
|
+
__name(warnIfUiMissing, "warnIfUiMissing");
|
|
3021
|
+
function defineModule(mountPath, resolvedUiRoot, theme, optionProviders, extraImports = []) {
|
|
3022
|
+
return {
|
|
3023
|
+
module: AdminModule,
|
|
3024
|
+
imports: [
|
|
3025
|
+
...extraImports,
|
|
3026
|
+
// The mount path is applied here, not on the controllers: `@Controller()`
|
|
3027
|
+
// is evaluated when the class is defined, long before either entry point
|
|
3028
|
+
// sees any options. `RouterModule` prefixes the module's routes and
|
|
3029
|
+
// preserves controller order, which the collision rule below depends on.
|
|
3030
|
+
import_core10.RouterModule.register([
|
|
3031
|
+
{
|
|
3032
|
+
path: mountPath,
|
|
3033
|
+
module: AdminModule
|
|
3034
|
+
}
|
|
3035
|
+
])
|
|
3036
|
+
],
|
|
3037
|
+
// Order matters and is the whole answer to the route collision. The UI
|
|
3038
|
+
// controller binds exactly two paths - the mount path itself and
|
|
3039
|
+
// `assets/:file` - and is matched first, so `assets` can never be read as a
|
|
3040
|
+
// model name. The auth controller claims `auth/*` next, for the same
|
|
3041
|
+
// reason and at the same cost: a model named `auth` is unreachable, as one
|
|
3042
|
+
// named `assets` or `actions` already was. Everything else falls through to
|
|
3043
|
+
// the API controller.
|
|
3044
|
+
controllers: [
|
|
3045
|
+
AdminUiController,
|
|
3046
|
+
AdminAuthController,
|
|
3047
|
+
AdminController
|
|
3048
|
+
],
|
|
3049
|
+
providers: [
|
|
3050
|
+
...optionProviders,
|
|
3051
|
+
{
|
|
3052
|
+
provide: ADMIN_UI_ROOT,
|
|
3053
|
+
useValue: resolvedUiRoot
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
provide: ADMIN_MOUNT_PATH,
|
|
3057
|
+
useValue: mountPath
|
|
3058
|
+
},
|
|
3059
|
+
{
|
|
3060
|
+
provide: ADMIN_THEME,
|
|
3061
|
+
useValue: theme
|
|
3062
|
+
},
|
|
3063
|
+
AdminService,
|
|
3064
|
+
// Provided so Nest can resolve them for `@UseGuards` / `@UseFilters` on
|
|
3065
|
+
// the controller. Deliberately not APP_GUARD or APP_FILTER: either would
|
|
3066
|
+
// take over behaviour for the whole host application rather than just the
|
|
3067
|
+
// admin routes.
|
|
3068
|
+
AdminAuthGuard,
|
|
3069
|
+
AdminExceptionFilter
|
|
3070
|
+
],
|
|
3071
|
+
exports: [
|
|
3072
|
+
AdminService
|
|
3073
|
+
]
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
__name(defineModule, "defineModule");
|
|
3077
|
+
var STRUCTURAL_OPTIONS = [
|
|
3078
|
+
"path",
|
|
3079
|
+
"uiRoot",
|
|
3080
|
+
"theme"
|
|
3081
|
+
];
|
|
3082
|
+
function assertNoStructuralOptions(resolved) {
|
|
3083
|
+
const misplaced = STRUCTURAL_OPTIONS.filter((key) => key in resolved);
|
|
3084
|
+
if (misplaced.length === 0) return;
|
|
3085
|
+
const one = misplaced.length === 1;
|
|
3086
|
+
throw new Error(`AdminModule.forRootAsync() received ${misplaced.join(", ")} from its factory. ${one ? "That option is" : "Those options are"} structural: routes are registered before any provider exists, so ${one ? "it" : "they"} must be passed to forRootAsync() itself, beside \`imports\` and \`inject\`, rather than returned from \`useFactory\`.`);
|
|
3087
|
+
}
|
|
3088
|
+
__name(assertNoStructuralOptions, "assertNoStructuralOptions");
|
|
3089
|
+
function optionsProviders(options) {
|
|
3090
|
+
const validate = /* @__PURE__ */ __name((resolved) => {
|
|
3091
|
+
assertNoStructuralOptions(resolved);
|
|
3092
|
+
assertUsableOptions(resolved, "forRootAsync");
|
|
3093
|
+
return resolved;
|
|
3094
|
+
}, "validate");
|
|
3095
|
+
if (options.useFactory) {
|
|
3096
|
+
return [
|
|
3097
|
+
{
|
|
3098
|
+
provide: ADMIN_OPTIONS,
|
|
3099
|
+
useFactory: /* @__PURE__ */ __name(async (...args) => validate(await options.useFactory(...args)), "useFactory"),
|
|
3100
|
+
inject: options.inject ?? []
|
|
3101
|
+
}
|
|
3102
|
+
];
|
|
3103
|
+
}
|
|
3104
|
+
const factoryClass = options.useExisting ?? options.useClass;
|
|
3105
|
+
return [
|
|
3106
|
+
// `useClass` has to be instantiated by Nest before it can be asked;
|
|
3107
|
+
// `useExisting` is already provided by the application.
|
|
3108
|
+
...options.useClass ? [
|
|
3109
|
+
{
|
|
3110
|
+
provide: options.useClass,
|
|
3111
|
+
useClass: options.useClass
|
|
3112
|
+
}
|
|
3113
|
+
] : [],
|
|
3114
|
+
{
|
|
3115
|
+
provide: ADMIN_OPTIONS,
|
|
3116
|
+
useFactory: /* @__PURE__ */ __name(async (factory) => validate(await factory.createAdminOptions()), "useFactory"),
|
|
3117
|
+
inject: [
|
|
3118
|
+
factoryClass
|
|
3119
|
+
]
|
|
3120
|
+
}
|
|
3121
|
+
];
|
|
3122
|
+
}
|
|
3123
|
+
__name(optionsProviders, "optionsProviders");
|
|
3124
|
+
var AdminModule = class {
|
|
3125
|
+
static {
|
|
3126
|
+
__name(this, "AdminModule");
|
|
3127
|
+
}
|
|
3128
|
+
static forRoot(options) {
|
|
3129
|
+
assertUsableOptions(options, "forRoot");
|
|
3130
|
+
const mountPath = normaliseMountPath(options.path);
|
|
3131
|
+
const resolvedUiRoot = options.uiRoot ?? uiRoot();
|
|
3132
|
+
warnIfUiMissing(resolvedUiRoot, mountPath);
|
|
3133
|
+
assertUsableTheme(options.theme);
|
|
3134
|
+
return defineModule(mountPath, resolvedUiRoot, options.theme, [
|
|
3135
|
+
{
|
|
3136
|
+
provide: ADMIN_ADAPTER,
|
|
3137
|
+
useValue: options.adapter
|
|
3138
|
+
},
|
|
3139
|
+
{
|
|
3140
|
+
provide: ADMIN_RESOURCES,
|
|
3141
|
+
useValue: options.resources
|
|
3142
|
+
},
|
|
3143
|
+
{
|
|
3144
|
+
provide: ADMIN_MODELS,
|
|
3145
|
+
useValue: options.models
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
provide: ADMIN_HOOKS,
|
|
3149
|
+
useValue: options.hooks
|
|
3150
|
+
},
|
|
3151
|
+
{
|
|
3152
|
+
provide: ADMIN_ACTIONS,
|
|
3153
|
+
useValue: options.actions
|
|
3154
|
+
},
|
|
3155
|
+
{
|
|
3156
|
+
provide: ADMIN_DASHBOARD,
|
|
3157
|
+
useValue: options.dashboard
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
provide: ADMIN_AUTH,
|
|
3161
|
+
useValue: options.auth
|
|
3162
|
+
},
|
|
3163
|
+
// Always provided, so injection resolves whether or not the consumer
|
|
3164
|
+
// supplied a policy. The default permits every model.
|
|
3165
|
+
{
|
|
3166
|
+
provide: ADMIN_RESOURCE_AUTH,
|
|
3167
|
+
useValue: options.resourceAuth ?? allowAllResources()
|
|
3168
|
+
}
|
|
3169
|
+
]);
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* The same module, with the adapter and the auth policy resolved through DI.
|
|
3173
|
+
*
|
|
3174
|
+
* For the ordinary case where those things are not available when the module
|
|
3175
|
+
* is declared: a `PrismaService` that belongs to another module, a connection
|
|
3176
|
+
* string that comes from `ConfigService`.
|
|
3177
|
+
*
|
|
3178
|
+
* ```ts
|
|
3179
|
+
* AdminModule.forRootAsync({
|
|
3180
|
+
* imports: [PrismaModule, ConfigModule],
|
|
3181
|
+
* inject: [PrismaService, ConfigService],
|
|
3182
|
+
* useFactory: (prisma: PrismaService, config: ConfigService) => ({
|
|
3183
|
+
* adapter: new PrismaAdapter({ client: prisma }),
|
|
3184
|
+
* auth: new SessionAdminAuth(config.get('ADMIN_ROLE')),
|
|
3185
|
+
* }),
|
|
3186
|
+
* })
|
|
3187
|
+
* ```
|
|
3188
|
+
*
|
|
3189
|
+
* `path` stays on this object rather than coming from the factory. Routes are
|
|
3190
|
+
* registered when the module is defined, which is before any provider has
|
|
3191
|
+
* been instantiated, so the mount path cannot wait for an injection - and a
|
|
3192
|
+
* `path` returned from the factory would be silently ignored, which is worse
|
|
3193
|
+
* than not offering it.
|
|
3194
|
+
*/
|
|
3195
|
+
static forRootAsync(options) {
|
|
3196
|
+
if (!options?.useFactory && !options?.useClass && !options?.useExisting) {
|
|
3197
|
+
throw new Error("AdminModule.forRootAsync() requires one of `useFactory`, `useClass` or `useExisting`. To configure the admin directly, use forRoot().");
|
|
3198
|
+
}
|
|
3199
|
+
const mountPath = normaliseMountPath(options.path);
|
|
3200
|
+
const resolvedUiRoot = options.uiRoot ?? uiRoot();
|
|
3201
|
+
warnIfUiMissing(resolvedUiRoot, mountPath);
|
|
3202
|
+
assertUsableTheme(options.theme);
|
|
3203
|
+
const derive2 = /* @__PURE__ */ __name((token, read) => ({
|
|
3204
|
+
provide: token,
|
|
3205
|
+
useFactory: read,
|
|
3206
|
+
inject: [
|
|
3207
|
+
ADMIN_OPTIONS
|
|
3208
|
+
]
|
|
3209
|
+
}), "derive");
|
|
3210
|
+
return defineModule(mountPath, resolvedUiRoot, options.theme, [
|
|
3211
|
+
...optionsProviders(options),
|
|
3212
|
+
derive2(ADMIN_ADAPTER, (resolved) => resolved.adapter),
|
|
3213
|
+
derive2(ADMIN_RESOURCES, (resolved) => resolved.resources),
|
|
3214
|
+
derive2(ADMIN_MODELS, (resolved) => resolved.models),
|
|
3215
|
+
derive2(ADMIN_HOOKS, (resolved) => resolved.hooks),
|
|
3216
|
+
derive2(ADMIN_ACTIONS, (resolved) => resolved.actions),
|
|
3217
|
+
derive2(ADMIN_DASHBOARD, (resolved) => resolved.dashboard),
|
|
3218
|
+
derive2(ADMIN_AUTH, (resolved) => resolved.auth),
|
|
3219
|
+
derive2(ADMIN_RESOURCE_AUTH, (resolved) => resolved.resourceAuth ?? allowAllResources())
|
|
3220
|
+
], options.imports ?? []);
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
AdminModule = _ts_decorate7([
|
|
3224
|
+
(0, import_common11.Module)({})
|
|
3225
|
+
], AdminModule);
|
|
3226
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3227
|
+
0 && (module.exports = {
|
|
3228
|
+
AdapterError,
|
|
3229
|
+
AdminModule,
|
|
3230
|
+
ConstraintError,
|
|
3231
|
+
FieldNotFoundError,
|
|
3232
|
+
ForbiddenError,
|
|
3233
|
+
InvalidQueryError,
|
|
3234
|
+
ModelNotFoundError,
|
|
3235
|
+
NestAdminError,
|
|
3236
|
+
RecordNotFoundError,
|
|
3237
|
+
UnauthorizedError,
|
|
3238
|
+
ValidationError,
|
|
3239
|
+
adminAccountOf,
|
|
3240
|
+
builtInAuth,
|
|
3241
|
+
generateSessionSecret,
|
|
3242
|
+
hashAdminPassword,
|
|
3243
|
+
isNestAdminError,
|
|
3244
|
+
unsafeAllowAllRequests,
|
|
3245
|
+
verifyAdminPassword
|
|
3246
|
+
});
|
|
3247
|
+
//# sourceMappingURL=index.cjs.map
|