admindb 2.1.1 → 2.3.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 +368 -335
- package/dist/app.js +227 -1
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js +109 -1
- package/dist/auth/crypto.js +98 -1
- package/dist/auth/index.js +21 -1
- package/dist/auth/middleware.js +78 -1
- package/dist/auth/routes.js +64 -1
- package/dist/auth/types.js +7 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +309 -1
- package/dist/cli/config.js +118 -1
- package/dist/cli/index.js +20 -1
- package/dist/cli/runner.js +208 -1
- package/dist/cli.js +4 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +130 -0
- package/dist/data/datasets.d.ts +1 -1
- package/dist/data/datasets.js +171 -1
- package/dist/data/detector.d.ts +1 -1
- package/dist/data/detector.js +535 -1
- package/dist/data/engine.d.ts +1 -1
- package/dist/data/engine.js +586 -1
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +30 -1
- package/dist/data/prng.d.ts +1 -0
- package/dist/data/prng.js +73 -0
- package/dist/data/registry.d.ts +1 -0
- package/dist/data/registry.js +805 -0
- package/dist/data/schema-graph.d.ts +1 -0
- package/dist/data/schema-graph.js +210 -0
- package/dist/data/strategies.d.ts +1 -1
- package/dist/data/strategies.js +100 -1
- package/dist/data/templates.d.ts +1 -0
- package/dist/data/templates.js +134 -0
- package/dist/data/types.d.ts +1 -1
- package/dist/data/types.js +5 -1
- package/dist/data/validator.d.ts +1 -0
- package/dist/data/validator.js +160 -0
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.js +472 -1
- package/dist/db/export.d.ts +1 -1
- package/dist/db/export.js +67 -1
- package/dist/db/filters.d.ts +1 -1
- package/dist/db/filters.js +119 -1
- package/dist/db/index.js +24 -1
- package/dist/db/introspection.js +164 -1
- package/dist/db/manager.js +327 -1
- package/dist/db/migrations.js +100 -1
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +687 -1
- package/dist/db/types.d.ts +1 -1
- package/dist/db/types.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +52 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -1
- package/dist/public/js/designer.js +1 -1
- package/dist/public/js/forms.js +1 -1
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/schema.js +1 -1
- package/dist/public/js/seed.js +1 -1
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +45 -0
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/helpers.js +213 -1
- package/dist/routes/api/import-export.js +82 -1
- package/dist/routes/api/index.js +48 -1
- package/dist/routes/api/query.js +74 -1
- package/dist/routes/api/rows.js +358 -1
- package/dist/routes/api/seed.js +167 -1
- package/dist/routes/api/tables.js +216 -1
- package/dist/routes/databases.js +210 -1
- package/dist/routes/index.js +19 -1
- package/dist/routes/pages.js +446 -1
- package/dist/serverless.js +149 -1
- package/dist/sql/classifier.js +24 -1
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.d.ts +1 -1
- package/dist/sql/generator.js +245 -1
- package/dist/sql/index.js +18 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/api.js +2 -1
- package/dist/utils/colors.js +54 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +185 -1
- package/dist/utils/csv.d.ts +1 -1
- package/dist/utils/csv.js +61 -1
- package/dist/utils/datatype.js +167 -1
- package/dist/utils/icons.js +81 -1
- package/dist/utils/index.js +20 -1
- package/dist/utils/logger.js +36 -1
- package/dist/views/layouts/main.hbs +1 -74
- package/dist/views/pages/databases.hbs +1 -233
- package/dist/views/pages/designer.hbs +1 -89
- package/dist/views/pages/erd.hbs +1 -0
- package/dist/views/pages/error.hbs +1 -10
- package/dist/views/pages/form.hbs +1 -59
- package/dist/views/pages/home.hbs +1 -124
- package/dist/views/pages/info.hbs +1 -0
- package/dist/views/pages/login.hbs +1 -98
- package/dist/views/pages/query.hbs +1 -87
- package/dist/views/pages/schema.hbs +1 -298
- package/dist/views/pages/seed-select.hbs +251 -0
- package/dist/views/pages/seed.hbs +273 -167
- package/dist/views/pages/table.hbs +1 -327
- package/dist/views/partials/navbar.hbs +1 -83
- package/dist/views/partials/sidebar.hbs +1 -117
- package/docs/API.md +149 -144
- package/docs/EXAMPLES.md +579 -576
- package/docs/SECURITY.md +265 -265
- package/package.json +79 -78
- package/dist/public/js/icons.js +0 -1
package/dist/app.js
CHANGED
|
@@ -1 +1,227 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createRouter = createRouter;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const express_handlebars_1 = require("express-handlebars");
|
|
10
|
+
const database_1 = require("./db/database");
|
|
11
|
+
const postgres_1 = require("./db/postgres");
|
|
12
|
+
const logger_1 = require("./utils/logger");
|
|
13
|
+
const pages_1 = require("./routes/pages");
|
|
14
|
+
const index_1 = require("./routes/api/index");
|
|
15
|
+
const databases_1 = require("./routes/databases");
|
|
16
|
+
const args_1 = require("./cli/args");
|
|
17
|
+
const common_1 = require("./utils/common");
|
|
18
|
+
const icons_1 = require("./utils/icons");
|
|
19
|
+
const serverless_1 = require("./serverless");
|
|
20
|
+
const auth_1 = require("./auth");
|
|
21
|
+
const APP_VERSION = (0, args_1.getPackageVersion)();
|
|
22
|
+
function createRouter(options = {}) {
|
|
23
|
+
return options.manager ? createManagerApp(options) : createSingleDbApp(options);
|
|
24
|
+
}
|
|
25
|
+
function setupViewEngine(app, _logger) {
|
|
26
|
+
app.engine('hbs', (0, express_handlebars_1.engine)({
|
|
27
|
+
extname: '.hbs',
|
|
28
|
+
defaultLayout: 'main',
|
|
29
|
+
layoutsDir: node_path_1.default.join(__dirname, 'views', 'layouts'),
|
|
30
|
+
partialsDir: node_path_1.default.join(__dirname, 'views', 'partials'),
|
|
31
|
+
helpers: {
|
|
32
|
+
eq: (a, b) => a === b,
|
|
33
|
+
startsWith: (a, b) => String(a ?? '').startsWith(String(b ?? '')),
|
|
34
|
+
isNull: (v) => v === null || v === undefined,
|
|
35
|
+
truncate: (v, n) => {
|
|
36
|
+
const s = v == null ? '' : String(v);
|
|
37
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
38
|
+
},
|
|
39
|
+
json: (v) => (JSON.stringify(v ?? null) ?? 'null').replace(/</g, '\\u003c'),
|
|
40
|
+
add: (a, b) => Number(a) + Number(b),
|
|
41
|
+
sub: (a, b) => Number(a) - Number(b),
|
|
42
|
+
gt: (a, b) => Number(a) > Number(b),
|
|
43
|
+
lt: (a, b) => Number(a) < Number(b),
|
|
44
|
+
join: (arr, sep) => (Array.isArray(arr) ? arr.join(String(sep ?? ',')) : String(arr ?? '')),
|
|
45
|
+
icon: (name, options) => {
|
|
46
|
+
const customClass = (options && options.hash && options.hash.class) || undefined;
|
|
47
|
+
return (0, icons_1.renderIcon)(String(name ?? ''), customClass);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
app.set('view engine', 'hbs');
|
|
52
|
+
app.set('views', node_path_1.default.join(__dirname, 'views'));
|
|
53
|
+
}
|
|
54
|
+
function addErrorHandlers(app, logger) {
|
|
55
|
+
app.use((req, res) => {
|
|
56
|
+
if (req.path.startsWith('/api/')) {
|
|
57
|
+
return res.status(404).json({ success: false, error: 'Not found.' });
|
|
58
|
+
}
|
|
59
|
+
res.status(404).render('pages/error', { title: 'Not found', status: 404, error: 'The page you requested does not exist.' });
|
|
60
|
+
});
|
|
61
|
+
app.use((err, req, res, _next) => {
|
|
62
|
+
logger.error('Unhandled error', err);
|
|
63
|
+
if (req.path.startsWith('/api/')) {
|
|
64
|
+
return res.status(500).json({ success: false, error: (0, common_1.errorMessage)(err) });
|
|
65
|
+
}
|
|
66
|
+
res.status(500).render('pages/error', { title: 'Error', status: 500, error: (0, common_1.errorMessage)(err) });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function createSingleDbApp(options) {
|
|
70
|
+
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
71
|
+
const rawTarget = options.connection || options.dbPath || '';
|
|
72
|
+
const isPg = Boolean(options.pgOptions) ||
|
|
73
|
+
(0, common_1.isPostgresConnectionString)(rawTarget) ||
|
|
74
|
+
options.db?.dialect === 'postgres';
|
|
75
|
+
const readonly = options.readonly !== undefined
|
|
76
|
+
? Boolean(options.readonly)
|
|
77
|
+
: (isServerless && !isPg);
|
|
78
|
+
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
79
|
+
const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
|
|
80
|
+
let db;
|
|
81
|
+
if (options.db) {
|
|
82
|
+
db = options.db;
|
|
83
|
+
}
|
|
84
|
+
else if (isPg) {
|
|
85
|
+
db = new postgres_1.PostgresDatabase(options.pgOptions || rawTarget, logger, { readonly });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
db = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly });
|
|
89
|
+
}
|
|
90
|
+
const databasesUrl = options.databasesUrl;
|
|
91
|
+
const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
|
|
92
|
+
const router = (0, express_1.default)();
|
|
93
|
+
router.disable('x-powered-by');
|
|
94
|
+
router.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
95
|
+
setupViewEngine(router, logger);
|
|
96
|
+
router.use(express_1.default.json({ limit: '10mb' }));
|
|
97
|
+
router.use(express_1.default.urlencoded({ extended: false }));
|
|
98
|
+
(0, auth_1.registerAuthRoutes)(router, authConfig, basePath, logger);
|
|
99
|
+
router.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
|
|
100
|
+
router.use(async (req, res, next) => {
|
|
101
|
+
try {
|
|
102
|
+
res.locals.basePath = basePath;
|
|
103
|
+
res.locals.currentPath = req.path;
|
|
104
|
+
res.locals.currentTable = null;
|
|
105
|
+
res.locals.dbPath = db.path;
|
|
106
|
+
res.locals.dbId = options.dbId ?? (db.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(db.path) || 'SQLite'));
|
|
107
|
+
res.locals.dialect = db.dialect;
|
|
108
|
+
res.locals.isPostgres = db.dialect === 'postgres';
|
|
109
|
+
res.locals.isSqlite = db.dialect === 'sqlite';
|
|
110
|
+
res.locals.dialectName = db.dialect === 'postgres' ? 'PostgreSQL' : 'SQLite';
|
|
111
|
+
res.locals.databasesUrl = databasesUrl ?? null;
|
|
112
|
+
res.locals.databasesMode = false;
|
|
113
|
+
res.locals.readonly = db.isReadOnly;
|
|
114
|
+
res.locals.serverless = isServerless;
|
|
115
|
+
res.locals.version = APP_VERSION;
|
|
116
|
+
res.locals.authEnabled = authConfig.enabled;
|
|
117
|
+
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
118
|
+
res.locals.icons = icons_1.ICONS;
|
|
119
|
+
if (!req.path.startsWith('/api/')) {
|
|
120
|
+
const all = await db.listTables();
|
|
121
|
+
const names = (all.data ?? []).map((t) => t.name);
|
|
122
|
+
res.locals.tables = names.filter((n) => !n.startsWith('_'));
|
|
123
|
+
res.locals.internalTables = names.filter((n) => n.startsWith('_'));
|
|
124
|
+
}
|
|
125
|
+
next();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
next(err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
(0, pages_1.registerPages)(router, { db, logger });
|
|
132
|
+
(0, index_1.registerApi)(router, { db, logger });
|
|
133
|
+
addErrorHandlers(router, logger);
|
|
134
|
+
return router;
|
|
135
|
+
}
|
|
136
|
+
function createManagerApp(options) {
|
|
137
|
+
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
138
|
+
const readonly = isServerless || Boolean(options.readonly);
|
|
139
|
+
const manager = options.manager;
|
|
140
|
+
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
141
|
+
const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
|
|
142
|
+
const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
|
|
143
|
+
const app = (0, express_1.default)();
|
|
144
|
+
app.disable('x-powered-by');
|
|
145
|
+
app.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
146
|
+
setupViewEngine(app, logger);
|
|
147
|
+
app.use(express_1.default.json({ limit: '10mb' }));
|
|
148
|
+
app.use(express_1.default.urlencoded({ extended: false }));
|
|
149
|
+
(0, auth_1.registerAuthRoutes)(app, authConfig, basePath, logger);
|
|
150
|
+
app.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
|
|
151
|
+
app.use(async (req, res, next) => {
|
|
152
|
+
try {
|
|
153
|
+
res.locals.basePath = basePath;
|
|
154
|
+
res.locals.currentPath = req.path;
|
|
155
|
+
res.locals.currentTable = null;
|
|
156
|
+
res.locals.dbPath = manager.directory;
|
|
157
|
+
res.locals.dbId = null;
|
|
158
|
+
res.locals.databasesUrl = null;
|
|
159
|
+
res.locals.databasesMode = true;
|
|
160
|
+
res.locals.readonly = readonly;
|
|
161
|
+
res.locals.serverless = isServerless;
|
|
162
|
+
res.locals.version = APP_VERSION;
|
|
163
|
+
res.locals.authEnabled = authConfig.enabled;
|
|
164
|
+
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
165
|
+
res.locals.icons = icons_1.ICONS;
|
|
166
|
+
if (!req.path.startsWith('/api/')) {
|
|
167
|
+
res.locals.tables = [];
|
|
168
|
+
res.locals.internalTables = [];
|
|
169
|
+
}
|
|
170
|
+
next();
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
next(err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
const subApps = new Map();
|
|
177
|
+
(0, databases_1.registerDatabasesRoutes)(app, {
|
|
178
|
+
manager,
|
|
179
|
+
logger,
|
|
180
|
+
basePath,
|
|
181
|
+
readonly,
|
|
182
|
+
allowBrowse: isServerless ? false : options.allowBrowse,
|
|
183
|
+
browseRoot: options.browseRoot,
|
|
184
|
+
invalidate: (id) => subApps.delete(id),
|
|
185
|
+
});
|
|
186
|
+
app.use('/:dbId', (req, res, next) => {
|
|
187
|
+
const dbId = String(req.params.dbId);
|
|
188
|
+
if (!manager.has(dbId)) {
|
|
189
|
+
if (req.path.startsWith('/api/')) {
|
|
190
|
+
return res.status(404).json({ success: false, error: `Database "${dbId}" does not exist.` });
|
|
191
|
+
}
|
|
192
|
+
return res.status(404).render('pages/error', {
|
|
193
|
+
title: 'Not found',
|
|
194
|
+
status: 404,
|
|
195
|
+
error: `Database "${dbId}" does not exist.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const rawRo = req.query.readonly;
|
|
199
|
+
if (rawRo !== undefined) {
|
|
200
|
+
const explicitRo = rawRo === '1' || rawRo === 'true';
|
|
201
|
+
if (!readonly || explicitRo) {
|
|
202
|
+
manager.setReadonly(dbId, explicitRo);
|
|
203
|
+
subApps.delete(dbId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
let sub = subApps.get(dbId);
|
|
207
|
+
if (!sub) {
|
|
208
|
+
const subBase = `${basePath || ''}/${encodeURIComponent(dbId)}`;
|
|
209
|
+
const effectiveRo = readonly || manager.isDbReadOnly(dbId);
|
|
210
|
+
sub = createSingleDbApp({
|
|
211
|
+
db: manager.open(dbId, effectiveRo),
|
|
212
|
+
basePath: subBase,
|
|
213
|
+
logger,
|
|
214
|
+
logLevel: options.logLevel,
|
|
215
|
+
dbId,
|
|
216
|
+
databasesUrl: `${basePath || ''}/`,
|
|
217
|
+
readonly: effectiveRo,
|
|
218
|
+
serverless: isServerless,
|
|
219
|
+
auth: authConfig,
|
|
220
|
+
});
|
|
221
|
+
subApps.set(dbId, sub);
|
|
222
|
+
}
|
|
223
|
+
return sub(req, res, next);
|
|
224
|
+
});
|
|
225
|
+
addErrorHandlers(app, logger);
|
|
226
|
+
return app;
|
|
227
|
+
}
|
package/dist/auth/config.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{type AuthConfig,type ResolvedAuthConfig} from './types';export declare
|
|
1
|
+
import{type AuthConfig,type ResolvedAuthConfig} from './types';export declare const resetPersistentSecretCache:()=>void;export declare function getOrCreatePersistentSecret(env?:NodeJS.ProcessEnv):string;export declare function resolveAuthConfig(optionsAuth?:boolean|AuthConfig,env?:NodeJS.ProcessEnv):ResolvedAuthConfig;
|
package/dist/auth/config.js
CHANGED
|
@@ -1 +1,109 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resetPersistentSecretCache = void 0;
|
|
7
|
+
exports.getOrCreatePersistentSecret = getOrCreatePersistentSecret;
|
|
8
|
+
exports.resolveAuthConfig = resolveAuthConfig;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const types_1 = require("./types");
|
|
14
|
+
let cachedFallbackSecret = null;
|
|
15
|
+
const resetPersistentSecretCache = () => {
|
|
16
|
+
cachedFallbackSecret = null;
|
|
17
|
+
};
|
|
18
|
+
exports.resetPersistentSecretCache = resetPersistentSecretCache;
|
|
19
|
+
function getOrCreatePersistentSecret(env = process.env) {
|
|
20
|
+
const envSecret = env.ADMINDB_SECRET ?? env.SESSION_SECRET;
|
|
21
|
+
if (envSecret && String(envSecret).trim()) {
|
|
22
|
+
return String(envSecret).trim();
|
|
23
|
+
}
|
|
24
|
+
const customSecretFile = env.ADMINDB_SECRET_FILE ? String(env.ADMINDB_SECRET_FILE).trim() : undefined;
|
|
25
|
+
if (!customSecretFile && cachedFallbackSecret)
|
|
26
|
+
return cachedFallbackSecret;
|
|
27
|
+
const candidatePaths = [];
|
|
28
|
+
if (customSecretFile) {
|
|
29
|
+
candidatePaths.push(node_path_1.default.resolve(customSecretFile));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
try {
|
|
33
|
+
const home = node_os_1.default.homedir();
|
|
34
|
+
if (home)
|
|
35
|
+
candidatePaths.push(node_path_1.default.join(home, '.admindb-secret'));
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
try {
|
|
39
|
+
const tmp = node_os_1.default.tmpdir();
|
|
40
|
+
if (tmp)
|
|
41
|
+
candidatePaths.push(node_path_1.default.join(tmp, '.admindb-secret'));
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
}
|
|
45
|
+
for (const filePath of candidatePaths) {
|
|
46
|
+
try {
|
|
47
|
+
if ((0, node_fs_1.existsSync)(filePath)) {
|
|
48
|
+
const content = (0, node_fs_1.readFileSync)(filePath, 'utf8').trim();
|
|
49
|
+
if (content.length >= 16) {
|
|
50
|
+
if (!customSecretFile)
|
|
51
|
+
cachedFallbackSecret = content;
|
|
52
|
+
return content;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
const newSecret = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
59
|
+
for (const filePath of candidatePaths) {
|
|
60
|
+
try {
|
|
61
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
62
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
63
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
64
|
+
(0, node_fs_1.writeFileSync)(filePath, newSecret, { encoding: 'utf8', mode: 0o600 });
|
|
65
|
+
if (!customSecretFile)
|
|
66
|
+
cachedFallbackSecret = newSecret;
|
|
67
|
+
return newSecret;
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
if (!customSecretFile)
|
|
72
|
+
cachedFallbackSecret = newSecret;
|
|
73
|
+
return newSecret;
|
|
74
|
+
}
|
|
75
|
+
function resolveAuthConfig(optionsAuth, env = process.env) {
|
|
76
|
+
const envDisabled = ['0', 'false', 'no', 'off'].includes(String(env.ADMINDB_AUTH ?? '').trim().toLowerCase()) ||
|
|
77
|
+
['1', 'true', 'yes'].includes(String(env.ADMINDB_NO_AUTH ?? env.ADMINDB_DISABLE_AUTH ?? '').trim().toLowerCase());
|
|
78
|
+
let enabled = true;
|
|
79
|
+
let username = String(env.ADMINDB_USERNAME ?? env.ADMINDB_USER ?? types_1.DEFAULT_USERNAME).trim();
|
|
80
|
+
let password = String(env.ADMINDB_PASSWORD ?? env.ADMINDB_PASS ?? types_1.DEFAULT_PASSWORD_HASH).trim();
|
|
81
|
+
let secret = String(env.ADMINDB_SECRET ?? env.SESSION_SECRET ?? '').trim() || getOrCreatePersistentSecret(env);
|
|
82
|
+
let sessionDurationMs = types_1.DEFAULT_SESSION_DURATION_MS;
|
|
83
|
+
if (optionsAuth === false || envDisabled) {
|
|
84
|
+
enabled = false;
|
|
85
|
+
}
|
|
86
|
+
else if (typeof optionsAuth === 'object' && optionsAuth !== null) {
|
|
87
|
+
if (optionsAuth.enabled === false)
|
|
88
|
+
enabled = false;
|
|
89
|
+
if (optionsAuth.username !== undefined)
|
|
90
|
+
username = String(optionsAuth.username).trim();
|
|
91
|
+
if (optionsAuth.password !== undefined)
|
|
92
|
+
password = String(optionsAuth.password).trim();
|
|
93
|
+
if (optionsAuth.secret !== undefined && String(optionsAuth.secret).trim()) {
|
|
94
|
+
secret = String(optionsAuth.secret).trim();
|
|
95
|
+
}
|
|
96
|
+
if (optionsAuth.sessionDurationMs !== undefined)
|
|
97
|
+
sessionDurationMs = optionsAuth.sessionDurationMs;
|
|
98
|
+
}
|
|
99
|
+
const isDefaultPassword = username === types_1.DEFAULT_USERNAME &&
|
|
100
|
+
(password === types_1.DEFAULT_PASSWORD_HASH || password === 'admin');
|
|
101
|
+
return {
|
|
102
|
+
enabled,
|
|
103
|
+
username,
|
|
104
|
+
password,
|
|
105
|
+
secret,
|
|
106
|
+
sessionDurationMs,
|
|
107
|
+
isDefaultPassword,
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/auth/crypto.js
CHANGED
|
@@ -1 +1,98 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hashPassword = hashPassword;
|
|
4
|
+
exports.constantTimeCompare = constantTimeCompare;
|
|
5
|
+
exports.verifyPassword = verifyPassword;
|
|
6
|
+
exports.parseCookies = parseCookies;
|
|
7
|
+
exports.createSessionToken = createSessionToken;
|
|
8
|
+
exports.verifySessionToken = verifySessionToken;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
function hashPassword(password, salt) {
|
|
12
|
+
const saltHex = salt || (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
13
|
+
const derivedKey = (0, node_crypto_1.scryptSync)(password, saltHex, 64).toString('hex');
|
|
14
|
+
return `scrypt:${saltHex}:${derivedKey}`;
|
|
15
|
+
}
|
|
16
|
+
function constantTimeCompare(a, b) {
|
|
17
|
+
try {
|
|
18
|
+
const bufA = Buffer.from(String(a), 'utf8');
|
|
19
|
+
const bufB = Buffer.from(String(b), 'utf8');
|
|
20
|
+
if (bufA.length !== bufB.length) {
|
|
21
|
+
(0, node_crypto_1.timingSafeEqual)(bufA, bufA);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return (0, node_crypto_1.timingSafeEqual)(bufA, bufB);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function verifyPassword(candidatePassword, storedPasswordOrHash) {
|
|
31
|
+
if (typeof candidatePassword !== 'string' || typeof storedPasswordOrHash !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
if (storedPasswordOrHash.startsWith('scrypt:')) {
|
|
34
|
+
const parts = storedPasswordOrHash.split(':');
|
|
35
|
+
if (parts.length === 3 && parts[1] && parts[2]) {
|
|
36
|
+
try {
|
|
37
|
+
const derivedKey = (0, node_crypto_1.scryptSync)(candidatePassword, parts[1], 64).toString('hex');
|
|
38
|
+
return constantTimeCompare(derivedKey, parts[2]);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return constantTimeCompare(candidatePassword, storedPasswordOrHash);
|
|
46
|
+
}
|
|
47
|
+
function parseCookies(cookieHeader) {
|
|
48
|
+
const cookies = {};
|
|
49
|
+
if (!cookieHeader)
|
|
50
|
+
return cookies;
|
|
51
|
+
for (const pair of cookieHeader.split(';')) {
|
|
52
|
+
const idx = pair.indexOf('=');
|
|
53
|
+
if (idx === -1)
|
|
54
|
+
continue;
|
|
55
|
+
const key = pair.slice(0, idx).trim();
|
|
56
|
+
let val = pair.slice(idx + 1).trim();
|
|
57
|
+
if (val.startsWith('"') && val.endsWith('"') && val.length >= 2)
|
|
58
|
+
val = val.slice(1, -1);
|
|
59
|
+
if (key) {
|
|
60
|
+
try {
|
|
61
|
+
cookies[key] = decodeURIComponent(val);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
cookies[key] = val;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return cookies;
|
|
69
|
+
}
|
|
70
|
+
function createSessionToken(username, secret, durationMs = types_1.DEFAULT_SESSION_DURATION_MS) {
|
|
71
|
+
const data = `${Buffer.from(username, 'utf8').toString('base64url')}.${Date.now() + durationMs}`;
|
|
72
|
+
const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(data).digest('base64url');
|
|
73
|
+
return `${data}.${signature}`;
|
|
74
|
+
}
|
|
75
|
+
function verifySessionToken(token, secret, expectedUser) {
|
|
76
|
+
if (!token || typeof token !== 'string')
|
|
77
|
+
return null;
|
|
78
|
+
const parts = token.split('.');
|
|
79
|
+
if (parts.length !== 3)
|
|
80
|
+
return null;
|
|
81
|
+
const [userPayload, expiresStr, providedSignature] = parts;
|
|
82
|
+
const data = `${userPayload}.${expiresStr}`;
|
|
83
|
+
const expectedSignature = (0, node_crypto_1.createHmac)('sha256', secret).update(data).digest('base64url');
|
|
84
|
+
if (!constantTimeCompare(providedSignature, expectedSignature))
|
|
85
|
+
return null;
|
|
86
|
+
const expiresAt = Number.parseInt(expiresStr, 10);
|
|
87
|
+
if (!Number.isFinite(expiresAt) || Date.now() > expiresAt)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const username = Buffer.from(userPayload, 'base64url').toString('utf8');
|
|
91
|
+
if (expectedUser && !constantTimeCompare(username, expectedUser))
|
|
92
|
+
return null;
|
|
93
|
+
return username;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/auth/index.js
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./crypto"), exports);
|
|
19
|
+
__exportStar(require("./config"), exports);
|
|
20
|
+
__exportStar(require("./middleware"), exports);
|
|
21
|
+
__exportStar(require("./routes"), exports);
|
package/dist/auth/middleware.js
CHANGED
|
@@ -1 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authenticateRequest = authenticateRequest;
|
|
4
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
const crypto_1 = require("./crypto");
|
|
7
|
+
function authenticateRequest(req, config) {
|
|
8
|
+
if (!config.enabled)
|
|
9
|
+
return { authenticated: true, user: 'anonymous' };
|
|
10
|
+
const authHeader = req.headers.authorization;
|
|
11
|
+
if (authHeader) {
|
|
12
|
+
const [scheme, credentials] = authHeader.split(/\s+/, 2);
|
|
13
|
+
if (scheme && credentials) {
|
|
14
|
+
const lowerScheme = scheme.toLowerCase();
|
|
15
|
+
if (lowerScheme === 'basic') {
|
|
16
|
+
try {
|
|
17
|
+
const decoded = Buffer.from(credentials, 'base64').toString('utf8');
|
|
18
|
+
const colonIdx = decoded.indexOf(':');
|
|
19
|
+
if (colonIdx !== -1) {
|
|
20
|
+
const user = decoded.slice(0, colonIdx);
|
|
21
|
+
const pass = decoded.slice(colonIdx + 1);
|
|
22
|
+
if ((0, crypto_1.constantTimeCompare)(user, config.username) && (0, crypto_1.verifyPassword)(pass, config.password)) {
|
|
23
|
+
return { authenticated: true, user };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (lowerScheme === 'bearer') {
|
|
31
|
+
const user = (0, crypto_1.verifySessionToken)(credentials, config.secret, config.username);
|
|
32
|
+
if (user)
|
|
33
|
+
return { authenticated: true, user };
|
|
34
|
+
if ((0, crypto_1.verifyPassword)(credentials, config.password)) {
|
|
35
|
+
return { authenticated: true, user: config.username };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const sessionCookie = (0, crypto_1.parseCookies)(req.headers.cookie)[types_1.SESSION_COOKIE_NAME];
|
|
41
|
+
if (sessionCookie) {
|
|
42
|
+
const user = (0, crypto_1.verifySessionToken)(sessionCookie, config.secret, config.username);
|
|
43
|
+
if (user)
|
|
44
|
+
return { authenticated: true, user };
|
|
45
|
+
}
|
|
46
|
+
return { authenticated: false };
|
|
47
|
+
}
|
|
48
|
+
function createAuthMiddleware(config, basePath, _logger) {
|
|
49
|
+
return (req, res, next) => {
|
|
50
|
+
res.locals.authEnabled = config.enabled;
|
|
51
|
+
res.locals.isDefaultPassword = config.isDefaultPassword;
|
|
52
|
+
if (!config.enabled) {
|
|
53
|
+
res.locals.authUser = null;
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
const p = req.path;
|
|
57
|
+
if (p.startsWith('/css/') || p.startsWith('/js/') || p === '/favicon.ico' || p === '/login' || p === '/logout') {
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
const auth = authenticateRequest(req, config);
|
|
61
|
+
if (auth.authenticated && auth.user) {
|
|
62
|
+
res.locals.authUser = auth.user;
|
|
63
|
+
req.user = auth.user;
|
|
64
|
+
return next();
|
|
65
|
+
}
|
|
66
|
+
if (p.startsWith('/api/') || req.xhr || req.headers.accept?.includes('application/json')) {
|
|
67
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="AdminDB", Bearer');
|
|
68
|
+
res.status(401).json({
|
|
69
|
+
success: false,
|
|
70
|
+
error: 'Authentication required. Please log in or provide valid credentials.',
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const returnUrl = req.originalUrl || `${basePath}${req.url}`;
|
|
75
|
+
const loginUrl = `${basePath}/login${returnUrl && returnUrl !== `${basePath}/` ? `?next=${encodeURIComponent(returnUrl)}` : ''}`;
|
|
76
|
+
res.redirect(loginUrl);
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/auth/routes.js
CHANGED
|
@@ -1 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerAuthRoutes = registerAuthRoutes;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const middleware_1 = require("./middleware");
|
|
6
|
+
const crypto_1 = require("./crypto");
|
|
7
|
+
function registerAuthRoutes(router, config, basePath, logger) {
|
|
8
|
+
router.get('/login', (req, res) => {
|
|
9
|
+
if (!config.enabled)
|
|
10
|
+
return res.redirect(`${basePath}/`);
|
|
11
|
+
const auth = (0, middleware_1.authenticateRequest)(req, config);
|
|
12
|
+
const nextUrl = String(req.query.next ?? '').trim();
|
|
13
|
+
if (auth.authenticated) {
|
|
14
|
+
return res.redirect(nextUrl.startsWith('/') ? nextUrl : `${basePath}/`);
|
|
15
|
+
}
|
|
16
|
+
res.render('pages/login', {
|
|
17
|
+
title: 'Sign in',
|
|
18
|
+
noSidebar: true,
|
|
19
|
+
next: nextUrl.startsWith('/') ? nextUrl : '',
|
|
20
|
+
isDefaultPassword: config.isDefaultPassword,
|
|
21
|
+
error: req.query.error ? String(req.query.error) : null,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
router.post('/login', (req, res) => {
|
|
25
|
+
if (!config.enabled)
|
|
26
|
+
return res.redirect(`${basePath}/`);
|
|
27
|
+
const username = String(req.body?.username ?? '').trim();
|
|
28
|
+
const password = String(req.body?.password ?? '').trim();
|
|
29
|
+
const nextUrl = String(req.body?.next ?? req.query.next ?? '').trim();
|
|
30
|
+
const isValid = (0, crypto_1.constantTimeCompare)(username, config.username) && (0, crypto_1.verifyPassword)(password, config.password);
|
|
31
|
+
if (!isValid) {
|
|
32
|
+
logger.warn(`Failed login attempt for user "${username || '<empty>'}"`);
|
|
33
|
+
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
|
34
|
+
return res.status(401).json({ success: false, error: 'Invalid username or password.' });
|
|
35
|
+
}
|
|
36
|
+
return res.status(401).render('pages/login', {
|
|
37
|
+
title: 'Sign in',
|
|
38
|
+
noSidebar: true,
|
|
39
|
+
username,
|
|
40
|
+
next: nextUrl.startsWith('/') ? nextUrl : '',
|
|
41
|
+
isDefaultPassword: config.isDefaultPassword,
|
|
42
|
+
error: 'Invalid username or password. Please check your credentials.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const token = (0, crypto_1.createSessionToken)(config.username, config.secret, config.sessionDurationMs);
|
|
46
|
+
const maxAgeSec = Math.floor(config.sessionDurationMs / 1000);
|
|
47
|
+
const expiresUtc = new Date(Date.now() + config.sessionDurationMs).toUTCString();
|
|
48
|
+
const cookiePath = basePath || '/';
|
|
49
|
+
res.setHeader('Set-Cookie', `${types_1.SESSION_COOKIE_NAME}=${encodeURIComponent(token)}; Path=${cookiePath}; Max-Age=${maxAgeSec}; Expires=${expiresUtc}; HttpOnly; SameSite=Lax`);
|
|
50
|
+
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
|
51
|
+
return res.json({ success: true, message: 'Logged in successfully.', user: config.username, token });
|
|
52
|
+
}
|
|
53
|
+
res.redirect(nextUrl.startsWith('/') ? nextUrl : `${basePath}/`);
|
|
54
|
+
});
|
|
55
|
+
const handleLogout = (req, res) => {
|
|
56
|
+
res.setHeader('Set-Cookie', `${types_1.SESSION_COOKIE_NAME}=; Path=${basePath || '/'}; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax`);
|
|
57
|
+
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
|
58
|
+
return res.json({ success: true, message: 'Logged out successfully.' });
|
|
59
|
+
}
|
|
60
|
+
res.redirect(`${basePath}/login`);
|
|
61
|
+
};
|
|
62
|
+
router.post('/logout', handleLogout);
|
|
63
|
+
router.get('/logout', handleLogout);
|
|
64
|
+
}
|