signatur 1.0.0 → 1.1.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/README.md +18 -11
- package/app.js +189 -19
- package/lib/engines/inkscape.js +211 -30
- package/lib/util/auth.js +157 -0
- package/lib/util/config.js +39 -0
- package/lib/util/index.js +2 -0
- package/lib/util/profile.js +22 -0
- package/package.json +27 -19
- package/static/js/bundle.js +791 -24
- package/static/js/main.js +249 -8
- package/static/js/plugins/calligraphy.js +196 -0
- package/static/js/plugins/collapsible.js +2 -2
- package/static/js/plugins/console.js +0 -1
- package/static/js/plugins/diagnostics.js +160 -0
- package/static/js/plugins/feedback.js +147 -0
- package/static/js/plugins/inspiration.js +4 -4
- package/static/js/plugins/modal.js +14 -1
- package/static/js/plugins/texteditor.js +8 -8
- package/static/js/plugins/viewportpreview.js +8 -0
- package/test/lib/smoke.js +322 -0
- package/test/lib/util/auth.js +78 -0
- package/test/lib/util/profile.js +43 -0
- package/views/forbidden-pt_pt.ejs +29 -0
- package/views/forbidden.ejs +29 -0
- package/views/head.ejs +6 -0
- package/views/login-pt_pt.ejs +42 -0
- package/views/login.ejs +42 -0
- package/views/manager-pt_pt.ejs +1 -1
- package/views/manager.ejs +1 -1
- package/views/report-pt_pt.ejs +3 -1
- package/views/report.ejs +3 -1
- package/views/settings-pt_pt.ejs +63 -1
- package/views/settings.ejs +63 -1
- package/views/signature-pt_pt.ejs +3 -1
- package/views/signature.ejs +3 -1
- package/views/viewport-pt_pt.ejs +66 -1
- package/views/viewport.ejs +66 -1
- package/views/welcome-pt_pt.ejs +7 -2
- package/views/welcome.ejs +7 -2
- package/CHANGELOG.md +0 -307
package/README.md
CHANGED
|
@@ -18,17 +18,24 @@ Supported file format include:
|
|
|
18
18
|
|
|
19
19
|
## Configuration
|
|
20
20
|
|
|
21
|
-
| Name
|
|
22
|
-
|
|
|
23
|
-
| `BASE_URL`
|
|
24
|
-
| `SIGNATUR_KEY`
|
|
25
|
-
| `HEADLESS_URL`
|
|
26
|
-
| `PRINT_URL`
|
|
27
|
-
| `PRINT_NODE`
|
|
28
|
-
| `PRINT_PRINTER`
|
|
29
|
-
| `PRINT_KEY`
|
|
30
|
-
| `ENGRAVE_NODE`
|
|
31
|
-
| `ENGRAVE_PRINTER`
|
|
21
|
+
| Name | Type | Default | Description |
|
|
22
|
+
| --------------------- | ------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `BASE_URL` | `str` | `http://localhost:3000` | The base URL that is going to be used in the construction of external URLs for Signatur. |
|
|
24
|
+
| `SIGNATUR_KEY` | `str` | `None` | Secret key that should be passed in protected calls so that the server side "trusts" the client side (authentication). |
|
|
25
|
+
| `HEADLESS_URL` | `str` | `https://headless.stage.hive.pt` | The base URL to be used to access [Headless](https://github.com/hivesolutions/headless). |
|
|
26
|
+
| `PRINT_URL` | `str` | `https://colony-print.stage.hive.pt` | Base URL of the [Colony Print](http://colony-print.hive.pt) service used for both the engraving job and the receipt printing. |
|
|
27
|
+
| `PRINT_NODE` | `str` | `default` | Name of the Colony Print node to use when printing the report receipt. |
|
|
28
|
+
| `PRINT_PRINTER` | `str` | `printer` | Name of the printer (within the receipt node) to use when printing the report receipt. |
|
|
29
|
+
| `PRINT_KEY` | `str` | `null` | Secret key used to authenticate against Colony Print; shared by both the engraving job and the receipt printing. |
|
|
30
|
+
| `ENGRAVE_NODE` | `str` | value of `PRINT_NODE` | Name of the Colony Print node to use when sending an engraving job; falls back to `PRINT_NODE` so existing single-printer deployments keep working. |
|
|
31
|
+
| `ENGRAVE_PRINTER` | `str` | value of `PRINT_PRINTER` | Name of the printer (within the engrave node) to use when sending an engraving job; falls back to `PRINT_PRINTER` for the same backward compat reason. |
|
|
32
|
+
| `FEATURE_CALLIGRAPHY` | `bool` | `false` | Base value of the calligraphy feature flag; when set to a truthy value (`1`, `true`, `yes`, `on`) the calligraphy mode controls render on `/viewport`. May be overridden per session through the `Features` tab on `/settings`. |
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
Every interactive route is gated behind a session login. The list of valid users lives in `config/users.json` (gitignored, with a `config/users.json.example` checked into the repository) as an array of `{ "username": "...", "password_hash": "$2a$...", "role": "admin" | "user" }` entries; the `role` controls whether the user can reach the admin-only surfaces (`/settings`, `/settings/diagnostics`, `/profiles/*`) or just the basic engraving flow.
|
|
37
|
+
|
|
38
|
+
New users are added through the `npm run user:add <username> <role>` helper which prompts for the password twice (no echo), bcrypts it at cost 10 and rewrites `config/users.json` in place; the running application picks the change up automatically through `fs.watch` so no restart is required. The bare `/login` and `/logout` routes, the `/info` endpoint, the static assets and the engine `/convert` endpoint (key authenticated through `SIGNATUR_KEY`) stay public.
|
|
32
39
|
|
|
33
40
|
## Query Parameters
|
|
34
41
|
|
package/app.js
CHANGED
|
@@ -7,7 +7,6 @@ const process = require("process");
|
|
|
7
7
|
const bodyParser = require("body-parser");
|
|
8
8
|
const multer = require("multer");
|
|
9
9
|
const JSZip = require("jszip");
|
|
10
|
-
const fetch = require("node-fetch");
|
|
11
10
|
const ejs = require("ejs");
|
|
12
11
|
const util = require("hive-js-util");
|
|
13
12
|
const info = require("./package");
|
|
@@ -60,6 +59,39 @@ app.locals.dev = process.env.NODE_ENV !== "production";
|
|
|
60
59
|
app.use("/static", express.static(path.join(__dirname, "static")));
|
|
61
60
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
62
61
|
|
|
62
|
+
// list of public route prefixes that bypass the global require
|
|
63
|
+
// user middleware so the login flow, the favicon and the public
|
|
64
|
+
// info endpoint can be reached without an authenticated session;
|
|
65
|
+
// the engine convert endpoint stays public so colony print can
|
|
66
|
+
// keep posting svgs validated through the existing key header,
|
|
67
|
+
// the static mount falls past express.static for missing assets
|
|
68
|
+
// so it must be public to keep returning a clean 404 instead of
|
|
69
|
+
// redirecting to /login, and the text route is hit by Headless
|
|
70
|
+
// from /receipt and /image without the user's cookie so it must
|
|
71
|
+
// also stay reachable for those rendering flows to work
|
|
72
|
+
const PUBLIC_PATHS = [
|
|
73
|
+
"/login",
|
|
74
|
+
"/logout",
|
|
75
|
+
"/info",
|
|
76
|
+
"/favicon.ico",
|
|
77
|
+
"/static",
|
|
78
|
+
"/convert",
|
|
79
|
+
"/text"
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// global authentication middleware that enforces a logged in
|
|
83
|
+
// user on every interactive route, allowing only the small list
|
|
84
|
+
// of public paths above through; admin only routes apply the
|
|
85
|
+
// `lib.requireAdmin` middleware directly below
|
|
86
|
+
app.use((req, res, next) => {
|
|
87
|
+
const isPublic = PUBLIC_PATHS.some(prefix => req.path === prefix || req.path.startsWith(prefix + "/"));
|
|
88
|
+
if (isPublic) {
|
|
89
|
+
next();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
lib.requireUser(req, res, next);
|
|
93
|
+
});
|
|
94
|
+
|
|
63
95
|
// configures the multer middleware that parses the multipart
|
|
64
96
|
// payload of the profile form, accepting only text fields since
|
|
65
97
|
// background images are now managed by the dedicated asset
|
|
@@ -91,6 +123,45 @@ app.get("/", (req, res, next) => {
|
|
|
91
123
|
res.redirect(302, home);
|
|
92
124
|
});
|
|
93
125
|
|
|
126
|
+
app.get("/login", (req, res, next) => {
|
|
127
|
+
const theme = req.query.theme || req.session.theme || "";
|
|
128
|
+
const locale = req.query.locale || req.session.locale || "";
|
|
129
|
+
const nextUrl = typeof req.query.next === "string" ? req.query.next : "";
|
|
130
|
+
const error = typeof req.query.error === "string" ? req.query.error : "";
|
|
131
|
+
res.render("login" + (locale ? `-${locale}` : ""), {
|
|
132
|
+
theme: theme,
|
|
133
|
+
next: nextUrl,
|
|
134
|
+
error: error
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.post("/login", async (req, res, next) => {
|
|
139
|
+
async function clojure() {
|
|
140
|
+
const username = typeof req.body.username === "string" ? req.body.username.trim() : "";
|
|
141
|
+
const password = typeof req.body.password === "string" ? req.body.password : "";
|
|
142
|
+
const nextRaw = typeof req.body.next === "string" ? req.body.next : "";
|
|
143
|
+
const safeNext = nextRaw.startsWith("/") && !nextRaw.startsWith("//") ? nextRaw : "/";
|
|
144
|
+
const user = await lib.verifyCredentials(username, password);
|
|
145
|
+
if (!user) {
|
|
146
|
+
const params = new URLSearchParams();
|
|
147
|
+
params.set("error", "invalid");
|
|
148
|
+
if (nextRaw) params.set("next", nextRaw);
|
|
149
|
+
res.redirect(302, "/login?" + params.toString());
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
req.session.user = user;
|
|
153
|
+
res.redirect(302, safeNext);
|
|
154
|
+
}
|
|
155
|
+
clojure().catch(next);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const handleLogout = (req, res) => {
|
|
159
|
+
if (req.session) req.session.user = null;
|
|
160
|
+
res.redirect(302, "/login");
|
|
161
|
+
};
|
|
162
|
+
app.get("/logout", handleLogout);
|
|
163
|
+
app.post("/logout", handleLogout);
|
|
164
|
+
|
|
94
165
|
app.get("/gateway", (req, res, next) => {
|
|
95
166
|
const fullscreen =
|
|
96
167
|
req.query.fullscreen !== undefined
|
|
@@ -144,7 +215,7 @@ app.post("/gateway", (req, res, next) => {
|
|
|
144
215
|
}
|
|
145
216
|
});
|
|
146
217
|
|
|
147
|
-
app.get("/settings", (req, res, next) => {
|
|
218
|
+
app.get("/settings", lib.requireAdmin, (req, res, next) => {
|
|
148
219
|
const fullscreen =
|
|
149
220
|
req.query.fullscreen !== undefined
|
|
150
221
|
? req.query.fullscreen === "1"
|
|
@@ -167,12 +238,18 @@ app.get("/settings", (req, res, next) => {
|
|
|
167
238
|
home: req.session.home === "welcome" ? "welcome" : "gateway",
|
|
168
239
|
showOptions: req.session.show_options !== "0",
|
|
169
240
|
viewportMode: req.session.viewport_mode === "store" ? "store" : "technical",
|
|
241
|
+
features: lib.resolveFeatures(req.session),
|
|
242
|
+
featuresBase: lib.conf.FEATURES || {},
|
|
243
|
+
featuresOverride: Object.keys(lib.FEATURES || {}).reduce((accumulator, name) => {
|
|
244
|
+
accumulator[name] = req.session["feature_" + name];
|
|
245
|
+
return accumulator;
|
|
246
|
+
}, {}),
|
|
170
247
|
next: nextUrl,
|
|
171
248
|
info: info || {}
|
|
172
249
|
});
|
|
173
250
|
});
|
|
174
251
|
|
|
175
|
-
app.post("/settings", (req, res, next) => {
|
|
252
|
+
app.post("/settings", lib.requireAdmin, (req, res, next) => {
|
|
176
253
|
const theme = req.body.theme || "";
|
|
177
254
|
const locale = req.body.locale || "";
|
|
178
255
|
req.session.theme = theme;
|
|
@@ -181,13 +258,27 @@ app.post("/settings", (req, res, next) => {
|
|
|
181
258
|
req.session.show_options = req.body.show_options === "0" ? "0" : "1";
|
|
182
259
|
req.session.viewport_mode = req.body.viewport_mode === "store" ? "store" : "technical";
|
|
183
260
|
|
|
261
|
+
// persists every declared feature flag override sent through the
|
|
262
|
+
// features tab onto the session, treating `1` / `0` as explicit
|
|
263
|
+
// overrides and anything else as a clear so the base value from
|
|
264
|
+
// the matching `FEATURE_<NAME>` env var takes over again
|
|
265
|
+
for (const name of Object.keys(lib.FEATURES || {})) {
|
|
266
|
+
const value = req.body["feature_" + name];
|
|
267
|
+
if (value === "1") req.session["feature_" + name] = "1";
|
|
268
|
+
else if (value === "0") req.session["feature_" + name] = "0";
|
|
269
|
+
else delete req.session["feature_" + name];
|
|
270
|
+
}
|
|
271
|
+
|
|
184
272
|
// resolves the redirect target from the submitted next field
|
|
185
273
|
// restricting it to local paths so the form cannot be used as
|
|
186
|
-
// an open redirect
|
|
274
|
+
// an open redirect, falling back to the bare `/` so the user
|
|
275
|
+
// lands on the configured home (gateway or welcome) according
|
|
276
|
+
// to the freshly saved `home` preference instead of being
|
|
277
|
+
// hardcoded to the welcome screen
|
|
187
278
|
const target =
|
|
188
279
|
typeof req.body.next === "string" && req.body.next.startsWith("/")
|
|
189
280
|
? req.body.next
|
|
190
|
-
: "/
|
|
281
|
+
: "/";
|
|
191
282
|
|
|
192
283
|
// persists the fullscreen flag onto the session so that it
|
|
193
284
|
// survives the next request without polluting the redirect
|
|
@@ -197,6 +288,18 @@ app.post("/settings", (req, res, next) => {
|
|
|
197
288
|
res.redirect(302, target);
|
|
198
289
|
});
|
|
199
290
|
|
|
291
|
+
app.post("/settings/diagnostics", lib.requireAdmin, (req, res, next) => {
|
|
292
|
+
async function clojure() {
|
|
293
|
+
const engine = lib.ENGINES.inkscape.singleton();
|
|
294
|
+
const probes = await engine.probe();
|
|
295
|
+
const fixturePath = path.join(__dirname, "res", "diagnostic.svg");
|
|
296
|
+
const svgBuffer = await fs.readFile(fixturePath);
|
|
297
|
+
const steps = await engine.diagnose(svgBuffer);
|
|
298
|
+
res.json({ probes: probes, steps: steps });
|
|
299
|
+
}
|
|
300
|
+
clojure().catch(next);
|
|
301
|
+
});
|
|
302
|
+
|
|
200
303
|
app.get("/welcome", (req, res, next) => {
|
|
201
304
|
const fullscreen =
|
|
202
305
|
req.query.fullscreen !== undefined
|
|
@@ -214,7 +317,8 @@ app.get("/welcome", (req, res, next) => {
|
|
|
214
317
|
masterb64: masterb64,
|
|
215
318
|
config: req.session.config || {},
|
|
216
319
|
showOptions: req.session.show_options !== "0",
|
|
217
|
-
info: info || {}
|
|
320
|
+
info: info || {},
|
|
321
|
+
user: req.session.user || null
|
|
218
322
|
});
|
|
219
323
|
});
|
|
220
324
|
|
|
@@ -231,7 +335,7 @@ app.get("/signature", (req, res, next) => {
|
|
|
231
335
|
res.render("signature" + (locale ? `-${locale}` : ""), {
|
|
232
336
|
fullscreen: fullscreen,
|
|
233
337
|
theme: theme,
|
|
234
|
-
back:
|
|
338
|
+
back: "/"
|
|
235
339
|
});
|
|
236
340
|
});
|
|
237
341
|
|
|
@@ -247,6 +351,7 @@ app.get("/viewport", (req, res, next) => {
|
|
|
247
351
|
req.session.locale = locale;
|
|
248
352
|
req.session.config = req.session.config || {};
|
|
249
353
|
req.session.config.text = req.query.text || req.session.config.text || null;
|
|
354
|
+
const features = lib.resolveFeatures(req.session);
|
|
250
355
|
res.render("viewport" + (locale ? `-${locale}` : ""), {
|
|
251
356
|
fullscreen: fullscreen,
|
|
252
357
|
theme: theme,
|
|
@@ -257,7 +362,9 @@ app.get("/viewport", (req, res, next) => {
|
|
|
257
362
|
config: req.session.config || {},
|
|
258
363
|
text: lib.deserializeText(req.session.config.text) || null,
|
|
259
364
|
viewportMode: req.session.viewport_mode === "store" ? "store" : "technical",
|
|
260
|
-
|
|
365
|
+
features: features,
|
|
366
|
+
featuresb64: Buffer.from(JSON.stringify(features)).toString("base64"),
|
|
367
|
+
back: "/"
|
|
261
368
|
});
|
|
262
369
|
});
|
|
263
370
|
|
|
@@ -282,7 +389,7 @@ app.get("/report", (req, res, next) => {
|
|
|
282
389
|
text: lib.deserializeText(req.session.config.text) || null,
|
|
283
390
|
font: lib.fontText(req.session.config.text) || null,
|
|
284
391
|
localize: (v, f) => lib.localize(v, locale || undefined, f),
|
|
285
|
-
back:
|
|
392
|
+
back: "/"
|
|
286
393
|
});
|
|
287
394
|
});
|
|
288
395
|
|
|
@@ -362,11 +469,74 @@ app.post("/convert", (req, res, next) => {
|
|
|
362
469
|
clojure().catch(next);
|
|
363
470
|
});
|
|
364
471
|
|
|
472
|
+
app.post("/feedback", (req, res, next) => {
|
|
473
|
+
async function clojure() {
|
|
474
|
+
// ensures the feedback feature is currently enabled for the
|
|
475
|
+
// requester so disabled deployments do not accept submissions
|
|
476
|
+
const features = lib.resolveFeatures(req.session);
|
|
477
|
+
if (!features.feedback) {
|
|
478
|
+
res.status(404).json({ error: "feedback feature is disabled" });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// validates the satisfaction value against the small set of
|
|
483
|
+
// allowed multiple choice answers so the on disk payload stays
|
|
484
|
+
// consistent and easy to aggregate later
|
|
485
|
+
const allowedSatisfaction = new Set([
|
|
486
|
+
"very_satisfied",
|
|
487
|
+
"satisfied",
|
|
488
|
+
"neutral",
|
|
489
|
+
"unsatisfied",
|
|
490
|
+
"very_unsatisfied"
|
|
491
|
+
]);
|
|
492
|
+
const satisfaction = typeof req.body.satisfaction === "string" ? req.body.satisfaction : "";
|
|
493
|
+
if (!allowedSatisfaction.has(satisfaction)) {
|
|
494
|
+
res.status(400).json({ error: "invalid satisfaction value" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// captures the optional free text notes, trimming any leading
|
|
499
|
+
// or trailing whitespace and applying a sensible upper bound
|
|
500
|
+
// so the persisted entries cannot grow unbounded
|
|
501
|
+
const notesRaw = typeof req.body.notes === "string" ? req.body.notes : "";
|
|
502
|
+
const notes = notesRaw.trim().slice(0, 2000);
|
|
503
|
+
|
|
504
|
+
// builds the entry payload, capturing the contextual fields the
|
|
505
|
+
// client sends so the feedback can be cross referenced with the
|
|
506
|
+
// engraving submission it relates to
|
|
507
|
+
const entry = {
|
|
508
|
+
id: crypto.randomUUID(),
|
|
509
|
+
timestamp: new Date().toISOString(),
|
|
510
|
+
satisfaction: satisfaction,
|
|
511
|
+
notes: notes,
|
|
512
|
+
profile: typeof req.body.profile === "string" ? req.body.profile : null,
|
|
513
|
+
variant: typeof req.body.variant === "string" ? req.body.variant : null,
|
|
514
|
+
locale: req.session.locale || null
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// groups the feedback submissions by year, month and day in
|
|
518
|
+
// separate subdirectories derived from the ISO timestamp so
|
|
519
|
+
// the on disk layout stays easy to navigate as the number of
|
|
520
|
+
// entries grows, while each entry remains its own JSON file
|
|
521
|
+
// named after the generated identifier for easy inspection
|
|
522
|
+
// and per file aggregation
|
|
523
|
+
const year = entry.timestamp.slice(0, 4);
|
|
524
|
+
const month = entry.timestamp.slice(5, 7);
|
|
525
|
+
const day = entry.timestamp.slice(8, 10);
|
|
526
|
+
const directoryPath = path.join(__dirname, "data", "feedback", year, month, day);
|
|
527
|
+
await fs.mkdir(directoryPath, { recursive: true });
|
|
528
|
+
const entryPath = path.join(directoryPath, `${entry.id}.json`);
|
|
529
|
+
await fs.writeFile(entryPath, JSON.stringify(entry, null, 4) + "\n", "utf8");
|
|
530
|
+
res.json({ id: entry.id });
|
|
531
|
+
}
|
|
532
|
+
clojure().catch(next);
|
|
533
|
+
});
|
|
534
|
+
|
|
365
535
|
app.get("/config", (req, res, next) => {
|
|
366
536
|
res.json(req.session.config || {});
|
|
367
537
|
});
|
|
368
538
|
|
|
369
|
-
app.get("/profiles/manager", (req, res, next) => {
|
|
539
|
+
app.get("/profiles/manager", lib.requireAdmin, (req, res, next) => {
|
|
370
540
|
async function clojure() {
|
|
371
541
|
const fullscreen =
|
|
372
542
|
req.query.fullscreen !== undefined
|
|
@@ -426,7 +596,7 @@ app.get("/profiles/manager", (req, res, next) => {
|
|
|
426
596
|
clojure().catch(next);
|
|
427
597
|
});
|
|
428
598
|
|
|
429
|
-
app.post("/profiles/validate", profileUpload, (req, res, next) => {
|
|
599
|
+
app.post("/profiles/validate", lib.requireAdmin, profileUpload, (req, res, next) => {
|
|
430
600
|
async function clojure() {
|
|
431
601
|
const editTarget = typeof req.body.edit_target === "string" ? req.body.edit_target : "";
|
|
432
602
|
const { profile, errors } = lib.validateProfileSubmission(
|
|
@@ -445,7 +615,7 @@ app.post("/profiles/validate", profileUpload, (req, res, next) => {
|
|
|
445
615
|
clojure().catch(next);
|
|
446
616
|
});
|
|
447
617
|
|
|
448
|
-
app.post("/profiles", profileUpload, (req, res, next) => {
|
|
618
|
+
app.post("/profiles", lib.requireAdmin, profileUpload, (req, res, next) => {
|
|
449
619
|
async function clojure() {
|
|
450
620
|
const profileText = req.body.profile_json || "";
|
|
451
621
|
const inspirationsText = req.body.inspirations_json || "";
|
|
@@ -507,7 +677,7 @@ app.post("/profiles", profileUpload, (req, res, next) => {
|
|
|
507
677
|
clojure().catch(next);
|
|
508
678
|
});
|
|
509
679
|
|
|
510
|
-
app.post("/profiles/:id/enabled", profileUpload, (req, res, next) => {
|
|
680
|
+
app.post("/profiles/:id/enabled", lib.requireAdmin, profileUpload, (req, res, next) => {
|
|
511
681
|
async function clojure() {
|
|
512
682
|
const id = req.params.id;
|
|
513
683
|
if (!lib.PROFILE_ID_PATTERN.test(id)) {
|
|
@@ -546,7 +716,7 @@ app.post("/profiles/:id/enabled", profileUpload, (req, res, next) => {
|
|
|
546
716
|
clojure().catch(next);
|
|
547
717
|
});
|
|
548
718
|
|
|
549
|
-
app.post("/profiles/:id/delete", (req, res, next) => {
|
|
719
|
+
app.post("/profiles/:id/delete", lib.requireAdmin, (req, res, next) => {
|
|
550
720
|
async function clojure() {
|
|
551
721
|
const id = req.params.id;
|
|
552
722
|
if (!lib.PROFILE_ID_PATTERN.test(id)) {
|
|
@@ -584,7 +754,7 @@ app.post("/profiles/:id/delete", (req, res, next) => {
|
|
|
584
754
|
clojure().catch(next);
|
|
585
755
|
});
|
|
586
756
|
|
|
587
|
-
app.get("/profiles/assets", (req, res, next) => {
|
|
757
|
+
app.get("/profiles/assets", lib.requireAdmin, (req, res, next) => {
|
|
588
758
|
async function clojure() {
|
|
589
759
|
const directoryPath = path.join(__dirname, "static", "profiles");
|
|
590
760
|
const files = await fs.readdir(directoryPath);
|
|
@@ -594,7 +764,7 @@ app.get("/profiles/assets", (req, res, next) => {
|
|
|
594
764
|
clojure().catch(next);
|
|
595
765
|
});
|
|
596
766
|
|
|
597
|
-
app.post("/profiles/assets", assetUpload, (req, res, next) => {
|
|
767
|
+
app.post("/profiles/assets", lib.requireAdmin, assetUpload, (req, res, next) => {
|
|
598
768
|
async function clojure() {
|
|
599
769
|
const errors = [];
|
|
600
770
|
|
|
@@ -651,7 +821,7 @@ app.post("/profiles/assets", assetUpload, (req, res, next) => {
|
|
|
651
821
|
clojure().catch(next);
|
|
652
822
|
});
|
|
653
823
|
|
|
654
|
-
app.post("/profiles/assets/:filename/delete", (req, res, next) => {
|
|
824
|
+
app.post("/profiles/assets/:filename/delete", lib.requireAdmin, (req, res, next) => {
|
|
655
825
|
async function clojure() {
|
|
656
826
|
const filename = req.params.filename;
|
|
657
827
|
if (!lib.ASSET_FILENAME_PATTERN.test(filename)) {
|
|
@@ -673,7 +843,7 @@ app.post("/profiles/assets/:filename/delete", (req, res, next) => {
|
|
|
673
843
|
clojure().catch(next);
|
|
674
844
|
});
|
|
675
845
|
|
|
676
|
-
app.get("/profiles/bundle", (req, res, next) => {
|
|
846
|
+
app.get("/profiles/bundle", lib.requireAdmin, (req, res, next) => {
|
|
677
847
|
async function clojure() {
|
|
678
848
|
const directoryPath = path.join(__dirname, "static", "profiles");
|
|
679
849
|
const files = await fs.readdir(directoryPath);
|
|
@@ -709,7 +879,7 @@ app.get("/profiles/bundle", (req, res, next) => {
|
|
|
709
879
|
clojure().catch(next);
|
|
710
880
|
});
|
|
711
881
|
|
|
712
|
-
app.post("/profiles/bundle", bundleUpload, (req, res, next) => {
|
|
882
|
+
app.post("/profiles/bundle", lib.requireAdmin, bundleUpload, (req, res, next) => {
|
|
713
883
|
async function clojure() {
|
|
714
884
|
if (!req.file) {
|
|
715
885
|
res.status(400).json({ error: "file is required" });
|