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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ import type { Db } from "./types.js";
2
+ export declare function sqlite(fileName: string): Db;
3
+ //# sourceMappingURL=db.d.ts.map
@@ -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
@@ -0,0 +1,5 @@
1
+ import Database from "better-sqlite3";
2
+ export function sqlite(fileName) {
3
+ return new Database(fileName);
4
+ }
5
+ //# sourceMappingURL=db.js.map
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"}
@@ -0,0 +1,6 @@
1
+ export declare class PanelError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function fail(message: string): never;
5
+ export declare function num(value: unknown, message?: string): number;
6
+ //# sourceMappingURL=errors.d.ts.map
@@ -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"}
@@ -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,4 @@
1
+ export { sqlite } from "./db.js";
2
+ export { fail, num, PanelError } from "./errors.js";
3
+ export { panel } from "./panel.js";
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -0,0 +1,3 @@
1
+ import type { PanelLabels } from "./types.js";
2
+ export declare function getLabels(config?: Partial<PanelLabels>): PanelLabels;
3
+ //# sourceMappingURL=labels.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,3 @@
1
+ import type { PanelConfig } from "./types.js";
2
+ export declare function panel(config: PanelConfig): import("express-serve-static-core").Express;
3
+ //# sourceMappingURL=panel.d.ts.map
@@ -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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"}
@@ -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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"}
@@ -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":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1C,MAAM,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC;AACnC,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AACxE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,WAAW,GACnB,MAAM,GACN;IACE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEN,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,GAAG,IAAI,CAAC;IACjD,UAAU,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC;IACrC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,EAAE,CAAC;IACP,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IAC/B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACtF,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,CAAC;CACb,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }