panel-free 1.0.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 +35 -0
- package/bin/panel-free.js +28 -0
- package/dist/data.d.ts +5 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +45 -0
- package/dist/data.js.map +1 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +5 -0
- package/dist/db.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +3 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +19 -0
- package/dist/labels.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +30 -0
- package/dist/logger.js.map +1 -0
- package/dist/panel.d.ts +3 -0
- package/dist/panel.d.ts.map +1 -0
- package/dist/panel.js +333 -0
- package/dist/panel.js.map +1 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +106 -0
- package/dist/styles.js.map +1 -0
- package/dist/templates.d.ts +33 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +160 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# panel-free
|
|
2
|
+
|
|
3
|
+
Учебная библиотека для простого веб-проекта на Node.js + Express + EJS + SQLite.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npm install panel-free
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
После установки пакет приносит с собой серверную часть, EJS, Express, better-sqlite3 и средство запуска TypeScript. В проекте остаётся написать только `main.ts` и `Service.ts`.
|
|
12
|
+
|
|
13
|
+
## Запуск двухфайлового проекта
|
|
14
|
+
|
|
15
|
+
Если в текущей папке есть `main.ts`, достаточно:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
npx panel-free
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Можно указать другой файл:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
npx panel-free src/main.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Пакет предоставляет команду `panel-free`, поэтому отдельный `npm install tsx` не нужен.
|
|
28
|
+
|
|
29
|
+
## Что находится внутри npm-пакета
|
|
30
|
+
|
|
31
|
+
Публичная упаковка содержит готовый `dist/` и `bin/`. Исходники библиотеки в npm-пакет намеренно не попадают: они остаются в архиве учебного проекта для изучения.
|
|
32
|
+
|
|
33
|
+
## Важно
|
|
34
|
+
|
|
35
|
+
Пакет сделан для учебного проекта. Пароли в демонстрационном приложении хранятся просто в SQLite, а сессии находятся в памяти процесса. Для реального production-сервиса нужна полноценная аутентификация и постоянное хранилище сессий.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const tsxCli = require.resolve("tsx/cli");
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const first = args[0];
|
|
10
|
+
const file = first && !first.startsWith("-") ? resolve(process.cwd(), args.shift()) : resolve(process.cwd(), "main.ts");
|
|
11
|
+
|
|
12
|
+
const child = spawn(process.execPath, [tsxCli, file, ...args], {
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
stdio: "inherit"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on("error", (error) => {
|
|
18
|
+
console.error(`panel-free: ${error.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
child.on("exit", (code, signal) => {
|
|
23
|
+
if (signal) {
|
|
24
|
+
process.kill(process.pid, signal);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.exit(code ?? 1);
|
|
28
|
+
});
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Db, FormConfig, PreparedField, PreparedView, Row } from "./types.js";
|
|
2
|
+
export declare function readRows(db: Db, sql: string): Row[];
|
|
3
|
+
export declare function readFields(db: Db, form: FormConfig): PreparedField[];
|
|
4
|
+
export declare function readViews(db: Db, views?: Record<string, string>): PreparedView[];
|
|
5
|
+
//# sourceMappingURL=data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAe,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,EAAgB,MAAM,YAAY,CAAC;AA4B9G,wBAAgB,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,CAEnD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,GAAG,aAAa,EAAE,CAapE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,YAAY,EAAE,CAKpF"}
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function titleFromName(name) {
|
|
2
|
+
return name
|
|
3
|
+
.replace(/Id$/, "")
|
|
4
|
+
.replace(/[_-]+/g, " ")
|
|
5
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/^./, (ch) => ch.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
function getFieldType(config) {
|
|
10
|
+
if (typeof config === "string")
|
|
11
|
+
return "select";
|
|
12
|
+
return config.type ?? (config.sql ? "select" : "text");
|
|
13
|
+
}
|
|
14
|
+
function getFieldSql(config) {
|
|
15
|
+
return typeof config === "string" ? config : config.sql;
|
|
16
|
+
}
|
|
17
|
+
function getFieldLabel(name, config) {
|
|
18
|
+
return typeof config === "string" ? titleFromName(name) : config.label ?? titleFromName(name);
|
|
19
|
+
}
|
|
20
|
+
function isAdminOnly(config) {
|
|
21
|
+
return typeof config !== "string" && Boolean(config.adminOnly);
|
|
22
|
+
}
|
|
23
|
+
export function readRows(db, sql) {
|
|
24
|
+
return db.prepare(sql).all();
|
|
25
|
+
}
|
|
26
|
+
export function readFields(db, form) {
|
|
27
|
+
return Object.entries(form.fields).map(([name, config]) => {
|
|
28
|
+
const type = getFieldType(config);
|
|
29
|
+
const sql = getFieldSql(config);
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
label: getFieldLabel(name, config),
|
|
33
|
+
type,
|
|
34
|
+
options: type === "select" && sql ? readRows(db, sql) : [],
|
|
35
|
+
adminOnly: isAdminOnly(config)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function readViews(db, views = {}) {
|
|
40
|
+
return Object.entries(views).map(([title, sql]) => ({
|
|
41
|
+
title,
|
|
42
|
+
rows: readRows(db, sql)
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=data.js.map
|
package/dist/data.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAEA,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI;SACR,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;SAClB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC;SACnC,IAAI,EAAE;SACN,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,YAAY,CAAC,MAAmB;IACvC,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAChD,OAAO,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB;IACtC,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;AAC1D,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,MAAmB;IACtD,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;AAChG,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB;IACtC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,EAAM,EAAE,GAAW;IAC1C,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAW,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAM,EAAE,IAAgB;IACjD,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE;QACxD,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAEhC,OAAO;YACL,IAAI;YACJ,KAAK,EAAE,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC;YAClC,IAAI;YACJ,OAAO,EAAE,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAoB,CAAC,CAAC,CAAC,EAAE;YAC9E,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC;SAC/B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAM,EAAE,QAAgC,EAAE;IAClE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QAClD,KAAK;QACL,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;KACxB,CAAC,CAAC,CAAC;AACN,CAAC"}
|
package/dist/db.d.ts
ADDED
package/dist/db.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAErC,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,EAAE,CAE3C"}
|
package/dist/db.js
ADDED
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAGtC,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAE3C;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,SAAuB,GAAG,MAAM,CAI1E"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class PanelError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "PanelError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function fail(message) {
|
|
8
|
+
throw new PanelError(message);
|
|
9
|
+
}
|
|
10
|
+
export function num(value, message = "Некорректное число") {
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
13
|
+
fail(message);
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,MAAM,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAc,EAAE,OAAO,GAAG,oBAAoB;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { sqlite } from "./db.js";
|
|
2
|
+
export { fail, num, PanelError } from "./errors.js";
|
|
3
|
+
export { panel } from "./panel.js";
|
|
4
|
+
export type { Db, FormConfig, PanelConfig, FieldConfig, PanelLabels, PanelLogger, PanelLogLevel, PreparedField, PreparedView, RequestBody, Row, SelectFieldConfig, SelectOption, AdminConfig, AuthConfig, PanelRole, PanelUser, RecordConfig } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EACV,EAAE,EACF,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,aAAa,EACb,aAAa,EACb,YAAY,EACZ,WAAW,EACX,GAAG,EACH,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,EACT,YAAY,EACb,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/labels.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkB9C,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAEpE"}
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const defaultLabels = {
|
|
2
|
+
home: "Главная",
|
|
3
|
+
data: "Данные",
|
|
4
|
+
reports: "Аналитика",
|
|
5
|
+
reportsTitle: "Аналитические запросы",
|
|
6
|
+
empty: "Нет данных",
|
|
7
|
+
defaultSubmit: "Сохранить",
|
|
8
|
+
login: "Вход",
|
|
9
|
+
register: "Регистрация",
|
|
10
|
+
logout: "Выйти",
|
|
11
|
+
admin: "Админ-панель",
|
|
12
|
+
manager: "Менеджер",
|
|
13
|
+
myRecords: "Мои заявки",
|
|
14
|
+
allRecords: "Все заявки"
|
|
15
|
+
};
|
|
16
|
+
export function getLabels(config) {
|
|
17
|
+
return { ...defaultLabels, ...config };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAgB;IACjC,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,WAAW;IACpB,YAAY,EAAE,uBAAuB;IACrC,KAAK,EAAE,YAAY;IACnB,aAAa,EAAE,WAAW;IAC1B,KAAK,EAAE,MAAM;IACb,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,OAAO;IACf,KAAK,EAAE,cAAc;IACrB,OAAO,EAAE,UAAU;IACnB,SAAS,EAAE,YAAY;IACvB,UAAU,EAAE,YAAY;CACzB,CAAC;AAEF,MAAM,UAAU,SAAS,CAAC,MAA6B;IACrD,OAAO,EAAE,GAAG,aAAa,EAAE,GAAG,MAAM,EAAE,CAAC;AACzC,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PanelLogger } from "./types.js";
|
|
2
|
+
export declare const consoleLogger: PanelLogger;
|
|
3
|
+
export declare function getLogger(config: boolean | PanelLogger | undefined): PanelLogger | null;
|
|
4
|
+
export declare function errorMeta(error: unknown): Record<string, unknown>;
|
|
5
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,aAAa,EAAE,WAkB3B,CAAC;AAEF,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,IAAI,CAIvF;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKjE"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const consoleLogger = {
|
|
2
|
+
log(level, message, meta) {
|
|
3
|
+
const time = new Date().toISOString();
|
|
4
|
+
const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
|
|
5
|
+
const line = `[panel-free] ${time} ${level.toUpperCase()} ${message}${suffix}`;
|
|
6
|
+
if (level === "error") {
|
|
7
|
+
console.error(line);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (level === "warn") {
|
|
11
|
+
console.warn(line);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(line);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export function getLogger(config) {
|
|
18
|
+
if (config === false)
|
|
19
|
+
return null;
|
|
20
|
+
if (config && typeof config === "object")
|
|
21
|
+
return config;
|
|
22
|
+
return consoleLogger;
|
|
23
|
+
}
|
|
24
|
+
export function errorMeta(error) {
|
|
25
|
+
if (error instanceof Error) {
|
|
26
|
+
return { name: error.name, message: error.message };
|
|
27
|
+
}
|
|
28
|
+
return { message: String(error) };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAgB;IACxC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI;QACtB,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,GAAG,gBAAgB,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,OAAO,GAAG,MAAM,EAAE,CAAC;QAE/E,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QAED,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,SAAS,CAAC,MAAyC;IACjE,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACxD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACpC,CAAC"}
|
package/dist/panel.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../src/panel.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,WAAW,EAAqC,MAAM,YAAY,CAAC;AA+C7F,wBAAgB,KAAK,CAAC,MAAM,EAAE,WAAW,+CAsRxC"}
|
package/dist/panel.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { readFields, readRows, readViews } from "./data.js";
|
|
3
|
+
import { getLabels } from "./labels.js";
|
|
4
|
+
import { errorMeta, getLogger } from "./logger.js";
|
|
5
|
+
import { adminTemplate, authTemplate, dataTemplate, pageTemplate, reportsTemplate } from "./templates.js";
|
|
6
|
+
const sessions = new Map();
|
|
7
|
+
function parseCookies(header) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const item of (header ?? "").split(";")) {
|
|
10
|
+
const [key, ...value] = item.trim().split("=");
|
|
11
|
+
if (key)
|
|
12
|
+
result[key] = decodeURIComponent(value.join("="));
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function makeSession(user) {
|
|
17
|
+
const token = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
|
|
18
|
+
sessions.set(token, user);
|
|
19
|
+
return token;
|
|
20
|
+
}
|
|
21
|
+
function currentUser(req) {
|
|
22
|
+
const token = parseCookies(req.headers.cookie).panel_session;
|
|
23
|
+
return token ? sessions.get(token) : undefined;
|
|
24
|
+
}
|
|
25
|
+
function requireUser(req, res) {
|
|
26
|
+
const user = currentUser(req);
|
|
27
|
+
if (!user) {
|
|
28
|
+
res.redirect("/login");
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return user;
|
|
32
|
+
}
|
|
33
|
+
function requireRole(req, res, roles) {
|
|
34
|
+
const user = requireUser(req, res);
|
|
35
|
+
if (!user)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (!roles.includes(user.role)) {
|
|
38
|
+
res.status(403).send("Доступ запрещён");
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return user;
|
|
42
|
+
}
|
|
43
|
+
function findAuth(config) {
|
|
44
|
+
return config.auth;
|
|
45
|
+
}
|
|
46
|
+
export function panel(config) {
|
|
47
|
+
const app = express();
|
|
48
|
+
const port = config.port ?? 3000;
|
|
49
|
+
const labels = getLabels(config.labels);
|
|
50
|
+
const logger = getLogger(config.logger);
|
|
51
|
+
const auth = findAuth(config);
|
|
52
|
+
const recordConfig = config.records;
|
|
53
|
+
const legacyForm = config.form;
|
|
54
|
+
app.use((req, res, next) => {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
res.on("finish", () => {
|
|
57
|
+
logger?.log("info", "request completed", {
|
|
58
|
+
method: req.method,
|
|
59
|
+
path: req.path,
|
|
60
|
+
status: res.statusCode,
|
|
61
|
+
durationMs: Date.now() - startedAt
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
next();
|
|
65
|
+
});
|
|
66
|
+
app.use(express.urlencoded({ extended: true }));
|
|
67
|
+
app.get("/login", (req, res) => {
|
|
68
|
+
if (!auth)
|
|
69
|
+
return res.redirect("/");
|
|
70
|
+
if (currentUser(req))
|
|
71
|
+
return res.redirect("/");
|
|
72
|
+
res.send(authTemplate({ title: `${config.title} — ${labels.login}`, labels, mode: "login", user: null, error: null, message: null }));
|
|
73
|
+
});
|
|
74
|
+
app.post("/login", (req, res) => {
|
|
75
|
+
if (!auth)
|
|
76
|
+
return res.redirect("/");
|
|
77
|
+
try {
|
|
78
|
+
const user = auth.onLogin(req.body);
|
|
79
|
+
if (!user) {
|
|
80
|
+
return res.status(400).send(authTemplate({
|
|
81
|
+
title: `${config.title} — ${labels.login}`,
|
|
82
|
+
labels,
|
|
83
|
+
mode: "login",
|
|
84
|
+
user: null,
|
|
85
|
+
error: "Неверный логин или пароль",
|
|
86
|
+
message: null
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
const token = makeSession(user);
|
|
90
|
+
res.setHeader("Set-Cookie", `panel_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax`);
|
|
91
|
+
return res.redirect("/");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger?.log("error", "login failed", errorMeta(error));
|
|
95
|
+
return res.status(400).send(authTemplate({
|
|
96
|
+
title: `${config.title} — ${labels.login}`,
|
|
97
|
+
labels,
|
|
98
|
+
mode: "login",
|
|
99
|
+
user: null,
|
|
100
|
+
error: error instanceof Error ? error.message : "Ошибка входа",
|
|
101
|
+
message: null
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
app.get("/register", (req, res) => {
|
|
106
|
+
if (!auth)
|
|
107
|
+
return res.redirect("/");
|
|
108
|
+
if (currentUser(req))
|
|
109
|
+
return res.redirect("/");
|
|
110
|
+
res.send(authTemplate({ title: `${config.title} — ${labels.register}`, labels, mode: "register", user: null, error: null, message: null }));
|
|
111
|
+
});
|
|
112
|
+
app.post("/register", (req, res) => {
|
|
113
|
+
if (!auth)
|
|
114
|
+
return res.redirect("/");
|
|
115
|
+
try {
|
|
116
|
+
const user = auth.onRegister(req.body);
|
|
117
|
+
const token = makeSession(user);
|
|
118
|
+
res.setHeader("Set-Cookie", `panel_session=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax`);
|
|
119
|
+
return res.redirect("/");
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger?.log("error", "registration failed", errorMeta(error));
|
|
123
|
+
return res.status(400).send(authTemplate({
|
|
124
|
+
title: `${config.title} — ${labels.register}`,
|
|
125
|
+
labels,
|
|
126
|
+
mode: "register",
|
|
127
|
+
user: null,
|
|
128
|
+
error: error instanceof Error ? error.message : "Ошибка регистрации",
|
|
129
|
+
message: null
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
app.post("/logout", (req, res) => {
|
|
134
|
+
const token = parseCookies(req.headers.cookie).panel_session;
|
|
135
|
+
if (token)
|
|
136
|
+
sessions.delete(token);
|
|
137
|
+
res.setHeader("Set-Cookie", "panel_session=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax");
|
|
138
|
+
res.redirect("/login");
|
|
139
|
+
});
|
|
140
|
+
app.get("/", (req, res) => {
|
|
141
|
+
const user = requireUser(req, res);
|
|
142
|
+
if (!user)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
const formConfig = user.role === "manager" ? undefined : (recordConfig?.form ?? legacyForm);
|
|
146
|
+
const fields = formConfig ? readFields(config.db, formConfig) : [];
|
|
147
|
+
const views = recordConfig
|
|
148
|
+
? readViews(config.db, { [user.role === "user" ? labels.myRecords : labels.allRecords]: recordConfig.listSql(user) })
|
|
149
|
+
: readViews(config.db, config.views);
|
|
150
|
+
res.send(pageTemplate({
|
|
151
|
+
title: config.title,
|
|
152
|
+
form: formConfig,
|
|
153
|
+
fields,
|
|
154
|
+
views,
|
|
155
|
+
labels,
|
|
156
|
+
user,
|
|
157
|
+
error: null,
|
|
158
|
+
message: null
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
logger?.log("error", "main page failed", errorMeta(error));
|
|
163
|
+
const formConfig = user.role === "manager" ? undefined : (recordConfig?.form ?? legacyForm);
|
|
164
|
+
res.status(400).send(pageTemplate({
|
|
165
|
+
title: config.title,
|
|
166
|
+
form: formConfig,
|
|
167
|
+
fields: formConfig ? readFields(config.db, formConfig) : [],
|
|
168
|
+
views: [],
|
|
169
|
+
labels,
|
|
170
|
+
user,
|
|
171
|
+
error: error instanceof Error ? error.message : "Ошибка страницы",
|
|
172
|
+
message: null
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
const submitForm = (req, res) => {
|
|
177
|
+
const user = requireRole(req, res, ["user", "admin"]);
|
|
178
|
+
if (!user)
|
|
179
|
+
return;
|
|
180
|
+
const form = recordConfig?.form ?? legacyForm;
|
|
181
|
+
if (!form)
|
|
182
|
+
return res.status(500).send("Форма не настроена");
|
|
183
|
+
try {
|
|
184
|
+
form.onSubmit(req.body, user);
|
|
185
|
+
res.redirect("/");
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
logger?.log("error", "form submit failed", errorMeta(error));
|
|
189
|
+
res.status(400).send(pageTemplate({
|
|
190
|
+
title: config.title,
|
|
191
|
+
form,
|
|
192
|
+
fields: readFields(config.db, form),
|
|
193
|
+
views: recordConfig
|
|
194
|
+
? readViews(config.db, { [user.role === "user" ? labels.myRecords : labels.allRecords]: recordConfig.listSql(user) })
|
|
195
|
+
: readViews(config.db, config.views),
|
|
196
|
+
labels,
|
|
197
|
+
user,
|
|
198
|
+
error: error instanceof Error ? error.message : "Ошибка сохранения",
|
|
199
|
+
message: null
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
if (recordConfig?.form)
|
|
204
|
+
app.post(recordConfig.form.action, submitForm);
|
|
205
|
+
if (legacyForm && !recordConfig)
|
|
206
|
+
app.post(legacyForm.action, submitForm);
|
|
207
|
+
app.get("/data", (req, res) => {
|
|
208
|
+
const user = requireUser(req, res);
|
|
209
|
+
if (!user)
|
|
210
|
+
return;
|
|
211
|
+
try {
|
|
212
|
+
const views = recordConfig
|
|
213
|
+
? readViews(config.db, { [user.role === "user" ? labels.myRecords : labels.allRecords]: recordConfig.listSql(user) })
|
|
214
|
+
: readViews(config.db, config.views);
|
|
215
|
+
res.send(dataTemplate({
|
|
216
|
+
title: `${config.title} — ${user.role === "user" ? labels.myRecords : labels.allRecords}`,
|
|
217
|
+
views,
|
|
218
|
+
labels,
|
|
219
|
+
user,
|
|
220
|
+
error: null,
|
|
221
|
+
message: null
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
logger?.log("error", "data page failed", errorMeta(error));
|
|
226
|
+
res.status(400).send(dataTemplate({
|
|
227
|
+
title: `${config.title} — ${labels.data}`,
|
|
228
|
+
views: [],
|
|
229
|
+
labels,
|
|
230
|
+
user,
|
|
231
|
+
error: error instanceof Error ? error.message : "Ошибка данных",
|
|
232
|
+
message: null
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
app.get("/reports", (req, res) => {
|
|
237
|
+
const user = requireRole(req, res, ["manager", "admin"]);
|
|
238
|
+
if (!user)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
res.send(reportsTemplate({
|
|
242
|
+
title: labels.reportsTitle,
|
|
243
|
+
reports: readViews(config.db, config.reports),
|
|
244
|
+
labels,
|
|
245
|
+
user,
|
|
246
|
+
error: null,
|
|
247
|
+
message: null
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
logger?.log("error", "reports rendering failed", errorMeta(error));
|
|
252
|
+
res.status(400).send(reportsTemplate({
|
|
253
|
+
title: labels.reportsTitle,
|
|
254
|
+
reports: [],
|
|
255
|
+
labels,
|
|
256
|
+
user,
|
|
257
|
+
error: error instanceof Error ? error.message : "Ошибка аналитики",
|
|
258
|
+
message: null
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
app.get("/admin", (req, res) => {
|
|
263
|
+
const user = requireRole(req, res, ["admin"]);
|
|
264
|
+
if (!user)
|
|
265
|
+
return;
|
|
266
|
+
if (!recordConfig?.admin)
|
|
267
|
+
return res.status(404).send("Админ-панель не настроена");
|
|
268
|
+
try {
|
|
269
|
+
const admin = recordConfig.admin;
|
|
270
|
+
res.send(adminTemplate({
|
|
271
|
+
title: `${config.title} — ${labels.admin}`,
|
|
272
|
+
rows: readRows(config.db, admin.listSql),
|
|
273
|
+
fields: readFields(config.db, { title: "admin", action: "/admin/update", fields: admin.fields, onSubmit: () => { } }),
|
|
274
|
+
labels,
|
|
275
|
+
user,
|
|
276
|
+
error: null,
|
|
277
|
+
message: null
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
logger?.log("error", "admin page failed", errorMeta(error));
|
|
282
|
+
res.status(400).send(adminTemplate({
|
|
283
|
+
title: `${config.title} — ${labels.admin}`,
|
|
284
|
+
rows: [],
|
|
285
|
+
fields: [],
|
|
286
|
+
labels,
|
|
287
|
+
user,
|
|
288
|
+
error: error instanceof Error ? error.message : "Ошибка админ-панели",
|
|
289
|
+
message: null
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
app.post("/admin/update", (req, res) => {
|
|
294
|
+
const user = requireRole(req, res, ["admin"]);
|
|
295
|
+
if (!user || !recordConfig?.admin)
|
|
296
|
+
return;
|
|
297
|
+
try {
|
|
298
|
+
const id = Number(req.body.id);
|
|
299
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
300
|
+
throw new Error("Некорректный ID записи");
|
|
301
|
+
recordConfig.admin.onUpdate(id, req.body);
|
|
302
|
+
res.redirect("/admin");
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
logger?.log("error", "admin update failed", errorMeta(error));
|
|
306
|
+
res.status(400).redirect("/admin");
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
app.post("/admin/delete", (req, res) => {
|
|
310
|
+
const user = requireRole(req, res, ["admin"]);
|
|
311
|
+
if (!user || !recordConfig?.admin)
|
|
312
|
+
return;
|
|
313
|
+
try {
|
|
314
|
+
const id = Number(req.body.id);
|
|
315
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
316
|
+
throw new Error("Некорректный ID записи");
|
|
317
|
+
recordConfig.admin.onDelete(id);
|
|
318
|
+
res.redirect("/admin");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
logger?.log("error", "admin delete failed", errorMeta(error));
|
|
322
|
+
res.status(400).redirect("/admin");
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
app.listen(port, () => {
|
|
326
|
+
logger?.log("info", "server started", {
|
|
327
|
+
url: `http://localhost:${port}`,
|
|
328
|
+
formAction: recordConfig?.form.action ?? legacyForm?.action
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
return app;
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.js","sourceRoot":"","sources":["../src/panel.ts"],"names":[],"mappings":"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"}
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const baseStyles = "\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n font-family: Arial, sans-serif;\n background: #f2f4f7;\n color: #222;\n line-height: 1.45;\n}\nmain {\n width: min(1080px, 100%);\n margin: 0 auto;\n padding: 28px 20px 40px;\n}\nheader {\n background: #fff;\n border: 1px solid #d9dee5;\n border-radius: 8px;\n padding: 20px;\n margin-bottom: 16px;\n}\nh1 { margin: 0 0 6px; font-size: 28px; }\n.subtitle { margin: 0; color: #697586; }\nnav {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 7px;\n margin: 0 0 18px;\n}\nnav a {\n display: inline-block;\n padding: 8px 12px;\n color: #1f5fbf;\n background: #fff;\n border: 1px solid #d7dde5;\n border-radius: 6px;\n text-decoration: none;\n}\nnav a:hover { background: #edf4ff; border-color: #b7c9e6; }\n.nav-gap { flex: 1; min-width: 10px; }\n.user-badge { color: #586575; font-size: 13px; }\n.logout { margin: 0; }\nsection, .auth-box {\n background: #fff;\n border: 1px solid #d9dee5;\n border-radius: 8px;\n padding: 18px;\n margin-bottom: 16px;\n}\nh2 { margin: 0 0 14px; font-size: 20px; }\nform {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\n gap: 12px;\n align-items: end;\n}\n.field { display: grid; gap: 6px; font-size: 14px; font-weight: 600; }\ninput, select, button {\n min-height: 40px;\n border: 1px solid #cbd3dd;\n border-radius: 6px;\n padding: 0 10px;\n font: inherit;\n background: #fff;\n}\nbutton {\n background: #2e6fbd;\n color: #fff;\n border-color: #2e6fbd;\n cursor: pointer;\n font-weight: 600;\n}\nbutton:hover { background: #255d9f; }\nbutton.secondary { background: #fff; color: #1f5fbf; border-color: #cbd3dd; }\nbutton.secondary:hover { background: #edf4ff; }\nbutton.danger { background: #b63a3a; border-color: #b63a3a; }\nbutton.danger:hover { background: #972f2f; }\n.table-wrap { overflow-x: auto; border: 1px solid #e1e5ea; border-radius: 6px; }\ntable { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 14px; }\nth, td { padding: 9px 10px; border-bottom: 1px solid #e5e8ec; text-align: left; vertical-align: top; }\nth { background: #f7f8fa; font-weight: 700; }\ntr:last-child td { border-bottom: 0; }\ntd { overflow-wrap: anywhere; }\n.notice { padding: 11px 13px; border-radius: 6px; margin-bottom: 14px; border: 1px solid; }\n.error { background: #fff0f0; color: #8e2626; border-color: #efc2c2; }\n.message { background: #effaf1; color: #27653b; border-color: #c8e6cf; }\n.empty { margin: 0; color: #737f8c; }\n.note { color: #697586; font-size: 13px; margin-top: 8px; }\n.auth-box { max-width: 520px; margin: 0 auto; }\n.auth-box form { grid-template-columns: 1fr; }\n.admin-list { display: grid; gap: 12px; }\n.admin-card { border: 1px solid #e1e5ea; border-radius: 7px; padding: 12px; }\n.admin-card form { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }\n.actions, .delete-form { display: flex; align-items: end; }\n.delete-form { margin-top: 10px; grid-template-columns: 1fr; }\n@media (max-width: 640px) {\n main { padding: 16px 12px 28px; }\n header, section, .auth-box { padding: 14px; }\n h1 { font-size: 23px; }\n h2 { font-size: 18px; }\n table { min-width: 480px; }\n .user-badge { width: 100%; order: 3; }\n}\n";
|
|
2
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,gsGAwGtB,CAAC"}
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const baseStyles = `
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body {
|
|
4
|
+
margin: 0;
|
|
5
|
+
font-family: Arial, sans-serif;
|
|
6
|
+
background: #f2f4f7;
|
|
7
|
+
color: #222;
|
|
8
|
+
line-height: 1.45;
|
|
9
|
+
}
|
|
10
|
+
main {
|
|
11
|
+
width: min(1080px, 100%);
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
padding: 28px 20px 40px;
|
|
14
|
+
}
|
|
15
|
+
header {
|
|
16
|
+
background: #fff;
|
|
17
|
+
border: 1px solid #d9dee5;
|
|
18
|
+
border-radius: 8px;
|
|
19
|
+
padding: 20px;
|
|
20
|
+
margin-bottom: 16px;
|
|
21
|
+
}
|
|
22
|
+
h1 { margin: 0 0 6px; font-size: 28px; }
|
|
23
|
+
.subtitle { margin: 0; color: #697586; }
|
|
24
|
+
nav {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: 7px;
|
|
29
|
+
margin: 0 0 18px;
|
|
30
|
+
}
|
|
31
|
+
nav a {
|
|
32
|
+
display: inline-block;
|
|
33
|
+
padding: 8px 12px;
|
|
34
|
+
color: #1f5fbf;
|
|
35
|
+
background: #fff;
|
|
36
|
+
border: 1px solid #d7dde5;
|
|
37
|
+
border-radius: 6px;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
}
|
|
40
|
+
nav a:hover { background: #edf4ff; border-color: #b7c9e6; }
|
|
41
|
+
.nav-gap { flex: 1; min-width: 10px; }
|
|
42
|
+
.user-badge { color: #586575; font-size: 13px; }
|
|
43
|
+
.logout { margin: 0; }
|
|
44
|
+
section, .auth-box {
|
|
45
|
+
background: #fff;
|
|
46
|
+
border: 1px solid #d9dee5;
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
padding: 18px;
|
|
49
|
+
margin-bottom: 16px;
|
|
50
|
+
}
|
|
51
|
+
h2 { margin: 0 0 14px; font-size: 20px; }
|
|
52
|
+
form {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
55
|
+
gap: 12px;
|
|
56
|
+
align-items: end;
|
|
57
|
+
}
|
|
58
|
+
.field { display: grid; gap: 6px; font-size: 14px; font-weight: 600; }
|
|
59
|
+
input, select, button {
|
|
60
|
+
min-height: 40px;
|
|
61
|
+
border: 1px solid #cbd3dd;
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
padding: 0 10px;
|
|
64
|
+
font: inherit;
|
|
65
|
+
background: #fff;
|
|
66
|
+
}
|
|
67
|
+
button {
|
|
68
|
+
background: #2e6fbd;
|
|
69
|
+
color: #fff;
|
|
70
|
+
border-color: #2e6fbd;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
button:hover { background: #255d9f; }
|
|
75
|
+
button.secondary { background: #fff; color: #1f5fbf; border-color: #cbd3dd; }
|
|
76
|
+
button.secondary:hover { background: #edf4ff; }
|
|
77
|
+
button.danger { background: #b63a3a; border-color: #b63a3a; }
|
|
78
|
+
button.danger:hover { background: #972f2f; }
|
|
79
|
+
.table-wrap { overflow-x: auto; border: 1px solid #e1e5ea; border-radius: 6px; }
|
|
80
|
+
table { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 14px; }
|
|
81
|
+
th, td { padding: 9px 10px; border-bottom: 1px solid #e5e8ec; text-align: left; vertical-align: top; }
|
|
82
|
+
th { background: #f7f8fa; font-weight: 700; }
|
|
83
|
+
tr:last-child td { border-bottom: 0; }
|
|
84
|
+
td { overflow-wrap: anywhere; }
|
|
85
|
+
.notice { padding: 11px 13px; border-radius: 6px; margin-bottom: 14px; border: 1px solid; }
|
|
86
|
+
.error { background: #fff0f0; color: #8e2626; border-color: #efc2c2; }
|
|
87
|
+
.message { background: #effaf1; color: #27653b; border-color: #c8e6cf; }
|
|
88
|
+
.empty { margin: 0; color: #737f8c; }
|
|
89
|
+
.note { color: #697586; font-size: 13px; margin-top: 8px; }
|
|
90
|
+
.auth-box { max-width: 520px; margin: 0 auto; }
|
|
91
|
+
.auth-box form { grid-template-columns: 1fr; }
|
|
92
|
+
.admin-list { display: grid; gap: 12px; }
|
|
93
|
+
.admin-card { border: 1px solid #e1e5ea; border-radius: 7px; padding: 12px; }
|
|
94
|
+
.admin-card form { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
|
95
|
+
.actions, .delete-form { display: flex; align-items: end; }
|
|
96
|
+
.delete-form { margin-top: 10px; grid-template-columns: 1fr; }
|
|
97
|
+
@media (max-width: 640px) {
|
|
98
|
+
main { padding: 16px 12px 28px; }
|
|
99
|
+
header, section, .auth-box { padding: 14px; }
|
|
100
|
+
h1 { font-size: 23px; }
|
|
101
|
+
h2 { font-size: 18px; }
|
|
102
|
+
table { min-width: 480px; }
|
|
103
|
+
.user-badge { width: 100%; order: 3; }
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwGzB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FormConfig, PanelLabels, PanelUser, PreparedField, PreparedView, Row } from "./types.js";
|
|
2
|
+
type Common = {
|
|
3
|
+
title: string;
|
|
4
|
+
labels: PanelLabels;
|
|
5
|
+
user?: PanelUser | null;
|
|
6
|
+
error?: string | null;
|
|
7
|
+
message?: string | null;
|
|
8
|
+
};
|
|
9
|
+
type AuthTemplateData = Common & {
|
|
10
|
+
mode: "login" | "register";
|
|
11
|
+
};
|
|
12
|
+
type HomeTemplateData = Common & {
|
|
13
|
+
form?: FormConfig | null;
|
|
14
|
+
fields?: PreparedField[] | null;
|
|
15
|
+
views: PreparedView[];
|
|
16
|
+
};
|
|
17
|
+
type DataTemplateData = Common & {
|
|
18
|
+
views: PreparedView[];
|
|
19
|
+
};
|
|
20
|
+
type ReportsTemplateData = Common & {
|
|
21
|
+
reports: PreparedView[];
|
|
22
|
+
};
|
|
23
|
+
type AdminTemplateData = Common & {
|
|
24
|
+
rows: Row[];
|
|
25
|
+
fields: PreparedField[];
|
|
26
|
+
};
|
|
27
|
+
export declare function authTemplate(data: AuthTemplateData): string;
|
|
28
|
+
export declare function pageTemplate(data: HomeTemplateData): string;
|
|
29
|
+
export declare function dataTemplate(data: DataTemplateData): string;
|
|
30
|
+
export declare function reportsTemplate(data: ReportsTemplateData): string;
|
|
31
|
+
export declare function adminTemplate(data: AdminTemplateData): string;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEvG,KAAK,MAAM,GAAG;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,KAAK,gBAAgB,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,GAAG,UAAU,CAAA;CAAE,CAAC;AAChE,KAAK,gBAAgB,GAAG,MAAM,GAAG;IAC/B,IAAI,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB,CAAC;AACF,KAAK,gBAAgB,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC;AAC3D,KAAK,mBAAmB,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC;AAChE,KAAK,iBAAiB,GAAG,MAAM,GAAG;IAChC,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB,CAAC;AAwDF,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAkB3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAyB3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAY3D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAYjE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqC7D"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import ejs from "ejs";
|
|
2
|
+
import { baseStyles } from "./styles.js";
|
|
3
|
+
const sharedHead = (title) => `<!doctype html>
|
|
4
|
+
<html lang="ru">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>${title}</title>
|
|
9
|
+
<style>${baseStyles}</style>
|
|
10
|
+
</head>`;
|
|
11
|
+
function navMarkup() {
|
|
12
|
+
return `<nav>
|
|
13
|
+
<% if (typeof user !== "undefined" && user) { %><a href="/"> <%= labels.home %> </a><% } %>
|
|
14
|
+
<% if (typeof user !== "undefined" && user) { %><a href="/data"><%= user.role === "user" ? labels.myRecords : labels.allRecords %></a><% } %>
|
|
15
|
+
<% if (typeof user !== "undefined" && user && typeof reportsAllowed !== "undefined" && reportsAllowed && user.role !== "user") { %><a href="/reports"><%= labels.reports %></a><% } %>
|
|
16
|
+
<% if (typeof user !== "undefined" && user && user.role === "admin") { %><a href="/admin"><%= labels.admin %></a><% } %>
|
|
17
|
+
<% if (typeof user !== "undefined" && user) { %>
|
|
18
|
+
<span class="nav-gap"></span>
|
|
19
|
+
<span class="user-badge"><%= user.name %> — <%= user.role %></span>
|
|
20
|
+
<form class="logout" method="post" action="/logout"><button type="submit" class="secondary"><%= labels.logout %></button></form>
|
|
21
|
+
<% } else { %>
|
|
22
|
+
<span class="nav-gap"></span><a href="/login"><%= labels.login %></a><a href="/register"><%= labels.register %></a>
|
|
23
|
+
<% } %>
|
|
24
|
+
</nav>`;
|
|
25
|
+
}
|
|
26
|
+
function tableMarkup(rowName) {
|
|
27
|
+
return `<div class="table-wrap"><table><thead><tr><% Object.keys(${rowName}.rows[0]).forEach(function(key) { %><th><%= key %></th><% }) %></tr></thead><tbody><% ${rowName}.rows.forEach(function(row) { %><tr><% Object.values(row).forEach(function(value) { %><td><%= value %></td><% }) %></tr><% }) %></tbody></table></div>`;
|
|
28
|
+
}
|
|
29
|
+
function fieldMarkup() {
|
|
30
|
+
return `<label class="field"><span><%= field.label %></span><% if (field.type === "hidden") { %><input type="hidden" name="<%= field.name %>"><% } else if (field.type === "select") { %><select name="<%= field.name %>" required><% field.options.forEach(function(option) { %><option value="<%= option.id %>"><%= option.name %></option><% }) %></select><% } else { %><input type="<%= field.type %>" name="<%= field.name %>" required><% } %></label>`;
|
|
31
|
+
}
|
|
32
|
+
function topMessage() {
|
|
33
|
+
return `<% if (typeof error !== "undefined" && error) { %><div class="notice error"><%= error %></div><% } %>
|
|
34
|
+
<% if (typeof message !== "undefined" && message) { %><div class="notice message"><%= message %></div><% } %>`;
|
|
35
|
+
}
|
|
36
|
+
function renderContext(data, extra = {}) {
|
|
37
|
+
return {
|
|
38
|
+
...data,
|
|
39
|
+
user: data.user ?? null,
|
|
40
|
+
error: data.error ?? null,
|
|
41
|
+
message: data.message ?? null,
|
|
42
|
+
reportsAllowed: false,
|
|
43
|
+
form: null,
|
|
44
|
+
fields: [],
|
|
45
|
+
views: [],
|
|
46
|
+
reports: [],
|
|
47
|
+
rows: [],
|
|
48
|
+
...extra
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function authTemplate(data) {
|
|
52
|
+
const isLogin = data.mode === "login";
|
|
53
|
+
return ejs.render(`${sharedHead(data.title)}
|
|
54
|
+
<body><main>
|
|
55
|
+
<header><h1><%= title %></h1><p class="subtitle">Простой вход в систему</p></header>
|
|
56
|
+
${navMarkup()}
|
|
57
|
+
${topMessage()}
|
|
58
|
+
<section class="auth-box">
|
|
59
|
+
<h2><%= mode === "login" ? labels.login : labels.register %></h2>
|
|
60
|
+
<form method="post" action="<%= mode === 'login' ? '/login' : '/register' %>">
|
|
61
|
+
<% if (!${isLogin}) { %><label class="field"><span>Имя</span><input type="text" name="name" required></label><% } %>
|
|
62
|
+
<label class="field"><span>Логин</span><input type="text" name="login" required></label>
|
|
63
|
+
<label class="field"><span>Пароль</span><input type="password" name="password" required></label>
|
|
64
|
+
<button type="submit"><%= mode === "login" ? "Войти" : "Зарегистрироваться" %></button>
|
|
65
|
+
</form>
|
|
66
|
+
<p class="note"><%= mode === "login" ? "Нет аккаунта? Используйте регистрацию." : "Новая учётная запись получает роль user." %></p>
|
|
67
|
+
</section>
|
|
68
|
+
</main></body></html>`, renderContext(data, { reportsAllowed: false }));
|
|
69
|
+
}
|
|
70
|
+
export function pageTemplate(data) {
|
|
71
|
+
return ejs.render(`${sharedHead(data.title)}
|
|
72
|
+
<body><main>
|
|
73
|
+
<header><h1><%= title %></h1><p class="subtitle">Описание проекта и основные операции</p></header>
|
|
74
|
+
${navMarkup()}
|
|
75
|
+
${topMessage()}
|
|
76
|
+
<% if (typeof form !== "undefined" && form && typeof fields !== "undefined" && fields && fields.length > 0) { %>
|
|
77
|
+
<section>
|
|
78
|
+
<h2><%= form.title %></h2>
|
|
79
|
+
<form method="post" action="<%= form.action %>">
|
|
80
|
+
<% fields.forEach(function(field) { %><% if (!field.adminOnly || (typeof user !== "undefined" && user && user.role === "admin")) { %>${fieldMarkup()}<% } %><% }) %>
|
|
81
|
+
<button type="submit"><%= form.submit || labels.defaultSubmit %></button>
|
|
82
|
+
</form>
|
|
83
|
+
<p class="note">Пользователь сохраняет заявку за своей учётной записью. Администратор может менять данные вручную.</p>
|
|
84
|
+
</section>
|
|
85
|
+
<% } %>
|
|
86
|
+
<% if (typeof user !== "undefined" && user && user.role === "manager") { %>
|
|
87
|
+
<section><h2>Режим менеджера</h2><p class="note">Менеджер видит все заявки и аналитику. Изменение и удаление доступны администратору.</p></section>
|
|
88
|
+
<% } %>
|
|
89
|
+
<% views.forEach(function(view) { %>
|
|
90
|
+
<section><h2><%= view.title %></h2>
|
|
91
|
+
<% if (view.rows.length === 0) { %><p class="empty"><%= labels.empty %></p><% } else { %>${tableMarkup("view")}<% } %>
|
|
92
|
+
</section>
|
|
93
|
+
<% }) %>
|
|
94
|
+
</main></body></html>`, renderContext(data, { reportsAllowed: true }));
|
|
95
|
+
}
|
|
96
|
+
export function dataTemplate(data) {
|
|
97
|
+
return ejs.render(`${sharedHead(data.title)}
|
|
98
|
+
<body><main>
|
|
99
|
+
<header><h1><%= title %></h1><p class="subtitle">Текущие данные</p></header>
|
|
100
|
+
${navMarkup()}
|
|
101
|
+
${topMessage()}
|
|
102
|
+
<% views.forEach(function(view) { %>
|
|
103
|
+
<section><h2><%= view.title %></h2>
|
|
104
|
+
<% if (view.rows.length === 0) { %><p class="empty"><%= labels.empty %></p><% } else { %>${tableMarkup("view")}<% } %>
|
|
105
|
+
</section>
|
|
106
|
+
<% }) %>
|
|
107
|
+
</main></body></html>`, renderContext(data, { reportsAllowed: true }));
|
|
108
|
+
}
|
|
109
|
+
export function reportsTemplate(data) {
|
|
110
|
+
return ejs.render(`${sharedHead(data.title)}
|
|
111
|
+
<body><main>
|
|
112
|
+
<header><h1><%= title %></h1><p class="subtitle">Примеры запросов для анализа данных</p></header>
|
|
113
|
+
${navMarkup()}
|
|
114
|
+
${topMessage()}
|
|
115
|
+
<% reports.forEach(function(report) { %>
|
|
116
|
+
<section><h2><%= report.title %></h2>
|
|
117
|
+
<% if (report.rows.length === 0) { %><p class="empty"><%= labels.empty %></p><% } else { %>${tableMarkup("report")}<% } %>
|
|
118
|
+
</section>
|
|
119
|
+
<% }) %>
|
|
120
|
+
</main></body></html>`, renderContext(data, { reportsAllowed: true }));
|
|
121
|
+
}
|
|
122
|
+
export function adminTemplate(data) {
|
|
123
|
+
return ejs.render(`${sharedHead(data.title)}
|
|
124
|
+
<body><main>
|
|
125
|
+
<header><h1><%= title %></h1><p class="subtitle">Просмотр, изменение и удаление заявок</p></header>
|
|
126
|
+
${navMarkup()}
|
|
127
|
+
${topMessage()}
|
|
128
|
+
<section>
|
|
129
|
+
<h2>Все заявки</h2>
|
|
130
|
+
<% if (rows.length === 0) { %><p class="empty"><%= labels.empty %></p><% } else { %>
|
|
131
|
+
<div class="admin-list">
|
|
132
|
+
<% rows.forEach(function(row) { %>
|
|
133
|
+
<article class="admin-card">
|
|
134
|
+
<form method="post" action="/admin/update">
|
|
135
|
+
<input type="hidden" name="id" value="<%= row.id %>">
|
|
136
|
+
<% fields.forEach(function(field) { %>
|
|
137
|
+
<label class="field"><span><%= field.label %></span>
|
|
138
|
+
<% if (field.type === "select") { %>
|
|
139
|
+
<select name="<%= field.name %>" required>
|
|
140
|
+
<% field.options.forEach(function(option) { %><option value="<%= option.id %>" <%= String(row[field.name]) === String(option.id) ? "selected" : "" %>><%= option.name %></option><% }) %>
|
|
141
|
+
</select>
|
|
142
|
+
<% } else { %>
|
|
143
|
+
<input type="<%= field.type %>" name="<%= field.name %>" value="<%= row[field.name] ?? "" %>" required>
|
|
144
|
+
<% } %>
|
|
145
|
+
</label>
|
|
146
|
+
<% }) %>
|
|
147
|
+
<div class="actions"><button type="submit">Изменить</button></div>
|
|
148
|
+
</form>
|
|
149
|
+
<form method="post" action="/admin/delete" class="delete-form" onsubmit="return confirm('Удалить запись?')">
|
|
150
|
+
<input type="hidden" name="id" value="<%= row.id %>">
|
|
151
|
+
<button type="submit" class="danger">Удалить</button>
|
|
152
|
+
</form>
|
|
153
|
+
</article>
|
|
154
|
+
<% }) %>
|
|
155
|
+
</div>
|
|
156
|
+
<% } %>
|
|
157
|
+
</section>
|
|
158
|
+
</main></body></html>`, renderContext(data, { reportsAllowed: true }));
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export type Row = Record<string, unknown>;
|
|
3
|
+
export type Db = Database.Database;
|
|
4
|
+
export type RequestBody = Record<string, string | string[] | undefined>;
|
|
5
|
+
export type PanelRole = "user" | "manager" | "admin";
|
|
6
|
+
export type PanelUser = {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
login: string;
|
|
10
|
+
role: PanelRole;
|
|
11
|
+
};
|
|
12
|
+
export type FieldConfig = string | {
|
|
13
|
+
type?: "text" | "number" | "date" | "select" | "hidden";
|
|
14
|
+
sql?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
adminOnly?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type SelectFieldConfig = FieldConfig;
|
|
19
|
+
export type FormConfig = {
|
|
20
|
+
title: string;
|
|
21
|
+
action: string;
|
|
22
|
+
submit?: string;
|
|
23
|
+
fields: Record<string, FieldConfig>;
|
|
24
|
+
onSubmit: (body: RequestBody, user: PanelUser) => void;
|
|
25
|
+
};
|
|
26
|
+
export type AuthConfig = {
|
|
27
|
+
onLogin: (body: RequestBody) => PanelUser | null;
|
|
28
|
+
onRegister: (body: RequestBody) => PanelUser;
|
|
29
|
+
};
|
|
30
|
+
export type AdminConfig = {
|
|
31
|
+
listSql: string;
|
|
32
|
+
fields: Record<string, FieldConfig>;
|
|
33
|
+
onUpdate: (id: number, body: RequestBody) => void;
|
|
34
|
+
onDelete: (id: number) => void;
|
|
35
|
+
};
|
|
36
|
+
export type RecordConfig = {
|
|
37
|
+
listSql: (user: PanelUser) => string;
|
|
38
|
+
form: FormConfig;
|
|
39
|
+
admin?: AdminConfig;
|
|
40
|
+
};
|
|
41
|
+
export type PanelLabels = {
|
|
42
|
+
home: string;
|
|
43
|
+
data: string;
|
|
44
|
+
reports: string;
|
|
45
|
+
reportsTitle: string;
|
|
46
|
+
empty: string;
|
|
47
|
+
defaultSubmit: string;
|
|
48
|
+
login: string;
|
|
49
|
+
register: string;
|
|
50
|
+
logout: string;
|
|
51
|
+
admin: string;
|
|
52
|
+
manager: string;
|
|
53
|
+
myRecords: string;
|
|
54
|
+
allRecords: string;
|
|
55
|
+
};
|
|
56
|
+
export type PanelConfig = {
|
|
57
|
+
title: string;
|
|
58
|
+
port?: number;
|
|
59
|
+
db: Db;
|
|
60
|
+
form?: FormConfig;
|
|
61
|
+
views?: Record<string, string>;
|
|
62
|
+
reports?: Record<string, string>;
|
|
63
|
+
labels?: Partial<PanelLabels>;
|
|
64
|
+
logger?: boolean | PanelLogger;
|
|
65
|
+
auth?: AuthConfig;
|
|
66
|
+
records?: RecordConfig;
|
|
67
|
+
};
|
|
68
|
+
export type PanelLogLevel = "info" | "warn" | "error";
|
|
69
|
+
export type PanelLogger = {
|
|
70
|
+
log: (level: PanelLogLevel, message: string, meta?: Record<string, unknown>) => void;
|
|
71
|
+
};
|
|
72
|
+
export type SelectOption = {
|
|
73
|
+
id: unknown;
|
|
74
|
+
name: unknown;
|
|
75
|
+
};
|
|
76
|
+
export type PreparedField = {
|
|
77
|
+
name: string;
|
|
78
|
+
label: string;
|
|
79
|
+
type: "text" | "number" | "date" | "select" | "hidden";
|
|
80
|
+
options: SelectOption[];
|
|
81
|
+
adminOnly?: boolean;
|
|
82
|
+
};
|
|
83
|
+
export type PreparedView = {
|
|
84
|
+
title: string;
|
|
85
|
+
rows: Row[];
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "panel-free",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Small reusable SQLite + Express + EJS web panel for student projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"panel-free": "bin/panel-free.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"bin",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20 <23"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"prepack": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"sqlite",
|
|
32
|
+
"express",
|
|
33
|
+
"ejs",
|
|
34
|
+
"student",
|
|
35
|
+
"panel"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"better-sqlite3": "11.10.0",
|
|
40
|
+
"ejs": "3.1.10",
|
|
41
|
+
"express": "5.1.0",
|
|
42
|
+
"tsx": "4.19.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/better-sqlite3": "9.6.0",
|
|
46
|
+
"@types/ejs": "^3.1.5",
|
|
47
|
+
"@types/express": "^5.0.6",
|
|
48
|
+
"@types/node": "^20.17.57",
|
|
49
|
+
"typescript": "^5.7.3"
|
|
50
|
+
}
|
|
51
|
+
}
|