wa-dev-tools/lib/admin.js
treamz 1837f42a91 AdminJS admin panel + dynamic content API
- AdminJS at /admin with auth (admin role only)
- Manage: users, categories, tools, content blocks, settings
- DB tables: settings, categories, tools, content_blocks
- Users table: added role (user/admin) and is_blocked fields
- API: /api/settings, /api/tools, /api/content/:section
- Sidebar: admin link visible only for admin users
- Removed /logs from public routes (now in AdminJS)
- Video converter: fixed ffmpeg codecs for RPi5 (h264_v4l2m2m)
- Dependencies: sequelize, @adminjs/sequelize, mariadb
2026-03-21 23:46:41 +03:00

181 lines
6.6 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'],
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 };