/** * 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 };