wa-dev-tools/lib/admin.js

237 lines
9.4 KiB
JavaScript

/**
* AdminJS setup — ESM modules loaded via dynamic import.
* Exports async function that mounts admin panel on the Express app.
*/
const { Sequelize, DataTypes } = require('sequelize');
// Sequelize connection (reuses same DB as lib/db.js)
const sequelize = new Sequelize(
process.env.DB_NAME || 'wa_tools',
process.env.DB_USER || 'wa_tools',
process.env.DB_PASSWORD || '',
{
host: process.env.DB_HOST || 'localhost',
dialect: 'mariadb',
logging: false,
}
);
// Define models
const User = sequelize.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
password_hash: { type: DataTypes.STRING(255), allowNull: false },
display_name: { type: DataTypes.STRING(100) },
role: { type: DataTypes.ENUM('user', 'admin'), defaultValue: 'user' },
is_blocked: { type: DataTypes.BOOLEAN, defaultValue: false },
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
last_login: { type: DataTypes.DATE },
}, { tableName: 'users', timestamps: false });
const Setting = sequelize.define('Setting', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
setting_key: { type: DataTypes.STRING(100), allowNull: false, unique: true },
setting_value: { type: DataTypes.TEXT },
description: { type: DataTypes.STRING(255) },
updated_at: { type: DataTypes.DATE },
}, { tableName: 'settings', timestamps: false });
const Category = sequelize.define('Category', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
slug: { type: DataTypes.STRING(50), allowNull: false, unique: true },
title: { type: DataTypes.STRING(100), allowNull: false },
description: { type: DataTypes.STRING(255) },
icon_svg: { type: DataTypes.TEXT },
color: { type: DataTypes.STRING(20), defaultValue: '#0054e6' },
sort_order: { type: DataTypes.INTEGER, defaultValue: 0 },
}, { tableName: 'categories', timestamps: false });
const Tool = sequelize.define('Tool', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
slug: { type: DataTypes.STRING(50), allowNull: false, unique: true },
title: { type: DataTypes.STRING(100), allowNull: false },
description: { type: DataTypes.STRING(255) },
path: { type: DataTypes.STRING(100), allowNull: false },
icon_svg: { type: DataTypes.TEXT },
category_id: { type: DataTypes.INTEGER },
sort_order: { type: DataTypes.INTEGER, defaultValue: 0 },
is_enabled: { type: DataTypes.BOOLEAN, defaultValue: true },
}, { tableName: 'tools', timestamps: false });
const ContentBlock = sequelize.define('ContentBlock', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
block_key: { type: DataTypes.STRING(100), allowNull: false, unique: true },
title: { type: DataTypes.STRING(255) },
body: { type: DataTypes.TEXT },
section: { type: DataTypes.STRING(50) },
sort_order: { type: DataTypes.INTEGER, defaultValue: 0 },
is_visible: { type: DataTypes.BOOLEAN, defaultValue: true },
updated_at: { type: DataTypes.DATE },
}, { tableName: 'content_blocks', timestamps: false });
Tool.belongsTo(Category, { foreignKey: 'category_id' });
Category.hasMany(Tool, { foreignKey: 'category_id' });
/**
* Mount AdminJS on the Express app at /admin
*/
async function setupAdmin(app) {
const AdminJS = (await import('adminjs')).default;
const AdminJSExpress = (await import('@adminjs/express')).default;
const AdminJSSequelize = (await import('@adminjs/sequelize')).default;
AdminJS.registerAdapter({
Resource: AdminJSSequelize.Resource,
Database: AdminJSSequelize.Database,
});
const adminJs = new AdminJS({
rootPath: '/admin',
loginPath: '/admin/login',
logoutPath: '/admin/logout',
branding: {
companyName: 'WA Dev Tools',
logo: false,
softwareBrothers: false,
},
resources: [
{
resource: User,
options: {
navigation: { name: 'Пользователи', icon: 'User' },
listProperties: ['id', 'email', 'display_name', 'role', 'is_blocked', 'last_login'],
editProperties: ['email', 'display_name', 'role', 'is_blocked', 'password', 'password_confirm'],
showProperties: ['id', 'email', 'display_name', 'role', 'is_blocked', 'created_at', 'last_login'],
properties: {
password_hash: { isVisible: false },
password: {
type: 'password',
isVisible: { list: false, show: false, edit: true, filter: false },
description: 'Оставьте пустым чтобы не менять',
},
password_confirm: {
type: 'password',
isVisible: { list: false, show: false, edit: true, filter: false },
description: 'Повторите пароль',
},
role: { availableValues: [{ value: 'user', label: 'User' }, { value: 'admin', label: 'Admin' }] },
},
actions: {
edit: {
before: async (request) => {
const pw = (request.payload?.password || '').trim();
const pw2 = (request.payload?.password_confirm || '').trim();
if (pw) {
if (pw2 && pw !== pw2) {
request.payload.errors = request.payload.errors || {};
request.payload.errors.password_confirm = { message: 'Пароли не совпадают' };
return request;
}
if (pw.length < 6) {
request.payload.errors = request.payload.errors || {};
request.payload.errors.password = { message: 'Минимум 6 символов' };
return request;
}
const bcrypt = require('bcrypt');
request.payload.password_hash = await bcrypt.hash(pw, 10);
}
delete request.payload.password;
delete request.payload.password_confirm;
return request;
},
},
new: {
before: async (request) => {
const pw = (request.payload?.password || '').trim();
const pw2 = (request.payload?.password_confirm || '').trim();
if (!pw) {
request.payload.errors = request.payload.errors || {};
request.payload.errors.password = { message: 'Пароль обязателен' };
return request;
}
if (pw2 && pw !== pw2) {
request.payload.errors = request.payload.errors || {};
request.payload.errors.password_confirm = { message: 'Пароли не совпадают' };
return request;
}
if (pw.length < 6) {
request.payload.errors = request.payload.errors || {};
request.payload.errors.password = { message: 'Минимум 6 символов' };
return request;
}
const bcrypt = require('bcrypt');
request.payload.password_hash = await bcrypt.hash(pw, 10);
delete request.payload.password;
delete request.payload.password_confirm;
return request;
},
},
},
},
},
{
resource: Category,
options: {
navigation: { name: 'Контент', icon: 'Grid' },
listProperties: ['id', 'slug', 'title', 'color', 'sort_order'],
sort: { sortBy: 'sort_order', direction: 'asc' },
},
},
{
resource: Tool,
options: {
navigation: { name: 'Контент', icon: 'Grid' },
listProperties: ['id', 'slug', 'title', 'path', 'category_id', 'is_enabled', 'sort_order'],
sort: { sortBy: 'sort_order', direction: 'asc' },
properties: {
icon_svg: { type: 'textarea' },
},
},
},
{
resource: ContentBlock,
options: {
navigation: { name: 'Контент', icon: 'Edit' },
listProperties: ['id', 'block_key', 'title', 'section', 'is_visible', 'sort_order'],
properties: {
body: { type: 'richtext' },
},
},
},
{
resource: Setting,
options: {
navigation: { name: 'Настройки', icon: 'Settings' },
listProperties: ['id', 'setting_key', 'setting_value', 'description'],
editProperties: ['setting_value', 'description'],
properties: {
setting_key: { isDisabled: true },
},
},
},
],
});
// Auth: use main site session — admin role required
const adminRouter = AdminJSExpress.buildRouter(adminJs);
// Middleware: check main session for admin role before AdminJS
app.use(adminJs.options.rootPath, (req, res, next) => {
// Allow AdminJS static assets (js, css, etc.)
if (req.path.match(/\.(js|css|png|svg|ico|map|woff|woff2|ttf)$/)) return next();
if (req.session && req.session.user && req.session.user.role === 'admin') {
return next();
}
// Not admin — redirect to login with return URL
req.session.returnTo = '/admin';
return res.redirect('/auth/login');
}, adminRouter);
return adminJs;
}
module.exports = { setupAdmin, sequelize, User, Setting, Category, Tool, ContentBlock };