- 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
181 lines
6.6 KiB
JavaScript
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 };
|