/** * 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'], showProperties: ['id', 'email', 'display_name', 'role', 'is_blocked', 'created_at', 'last_login'], properties: { password_hash: { isVisible: false }, role: { availableValues: [{ value: 'user', label: 'User' }, { value: 'admin', label: 'Admin' }] }, }, }, }, { 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: only admin users const bcrypt = require('bcrypt'); const db = require('./db'); const adminRouter = AdminJSExpress.buildAuthenticatedRouter(adminJs, { authenticate: async (email, password) => { const user = await db.findUserByEmail(email); if (!user || user.role !== 'admin') return null; const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return null; return { email: user.email, role: user.role, id: user.id }; }, cookieName: 'adminjs', cookiePassword: process.env.SESSION_SECRET || 'adminjs-secret-change-me', }, null, { resave: false, saveUninitialized: false, secret: process.env.SESSION_SECRET || 'adminjs-secret-change-me', }); app.use(adminJs.options.rootPath, adminRouter); return adminJs; } module.exports = { setupAdmin, sequelize, User, Setting, Category, Tool, ContentBlock };