Last git push by Hasan ( create the table in DB for sava user Theme)
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import settingsRouter from './settings/setting.routes.js';
|
||||
import themeRouter from './theme/routes/theme.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/settings/theme', themeRouter);
|
||||
router.use('/settings', settingsRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,7 +3,6 @@ import controller from './setting.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../shared/middleware/validation.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
import { audit } from '../../../shared/middleware/audit.middleware.js';
|
||||
import {
|
||||
createValidation,
|
||||
updateValidation,
|
||||
@@ -59,11 +58,20 @@ router.get(
|
||||
* @swagger
|
||||
* /api/v1/settings:
|
||||
* post:
|
||||
* summary: Create a setting
|
||||
* summary: Create a new setting
|
||||
* tags: [Settings]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Success
|
||||
* description: Created
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
@@ -71,7 +79,6 @@ router.post(
|
||||
authorize(['settings.users']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_SETTING'),
|
||||
controller.create
|
||||
);
|
||||
|
||||
@@ -79,12 +86,23 @@ router.post(
|
||||
* @swagger
|
||||
* /api/v1/settings/{id}:
|
||||
* put:
|
||||
* summary: Update a setting
|
||||
* summary: Update an existing setting
|
||||
* tags: [Settings]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
@@ -95,7 +113,6 @@ router.put(
|
||||
authorize(['settings.users']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_SETTING'),
|
||||
controller.update
|
||||
);
|
||||
|
||||
@@ -109,6 +126,8 @@ router.put(
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
@@ -119,7 +138,6 @@ router.delete(
|
||||
authorize(['settings.users']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_SETTING'),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const DEFAULT_THEME = 'royal-purple';
|
||||
|
||||
export const ALLOWED_THEME_CODES = [
|
||||
'royal-purple',
|
||||
'forest-green',
|
||||
'ocean-blue',
|
||||
'sunset-orange',
|
||||
'dark'
|
||||
];
|
||||
|
||||
export const SYSTEM_THEMES = ['royal-purple', 'dark'];
|
||||
@@ -0,0 +1,31 @@
|
||||
import service from '../services/theme.service.js';
|
||||
|
||||
export class ThemeController {
|
||||
getCurrentTheme = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
const data = await service.getCurrentTheme(userId);
|
||||
return res.status(200).json({ success: true, data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentTheme = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
const { themeCode } = req.body;
|
||||
const requestContext = {
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent']
|
||||
};
|
||||
await service.updateCurrentTheme(userId, themeCode, requestContext);
|
||||
return res.status(200).json({ success: true, message: 'Theme updated successfully.' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ThemeController();
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as themeRoutes } from './routes/theme.routes.js';
|
||||
export { default as themeController } from './controllers/theme.controller.js';
|
||||
export { default as themeService } from './services/theme.service.js';
|
||||
export { default as themeRepository } from './repositories/theme.repository.js';
|
||||
export { default as userThemeModel } from './models/userTheme.model.js';
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class UserTheme extends Model {
|
||||
static associate(models) {
|
||||
UserTheme.belongsTo(models.User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user',
|
||||
onDelete: 'CASCADE'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
UserTheme.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
theme_code: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'UserTheme',
|
||||
tableName: 'user_themes',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return UserTheme;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { models } from '../../../../shared/database/models.js';
|
||||
|
||||
export class ThemeRepository {
|
||||
async findByUserId(userId, options = {}) {
|
||||
return await models.UserTheme.findOne({
|
||||
where: { user_id: userId },
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.UserTheme.create(data, options);
|
||||
}
|
||||
|
||||
async update(userId, themeCode, options = {}) {
|
||||
const record = await this.findByUserId(userId, options);
|
||||
if (!record) return null;
|
||||
return await record.update({ theme_code: themeCode }, options);
|
||||
}
|
||||
|
||||
async save(userThemeInstance, options = {}) {
|
||||
return await userThemeInstance.save(options);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ThemeRepository();
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import controller from '../controllers/theme.controller.js';
|
||||
import { authenticate } from '../../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../../shared/middleware/validation.middleware.js';
|
||||
import { themeValidation } from '../validators/theme.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/settings/theme:
|
||||
* get:
|
||||
* summary: Retrieve user theme preference
|
||||
* tags: [Theme]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
controller.getCurrentTheme
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/v1/settings/theme:
|
||||
* put:
|
||||
* summary: Update user theme preference
|
||||
* tags: [Theme]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
router.put(
|
||||
'/',
|
||||
authenticate,
|
||||
themeValidation,
|
||||
validate,
|
||||
controller.updateCurrentTheme
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from '../repositories/theme.repository.js';
|
||||
import { ALLOWED_THEME_CODES, DEFAULT_THEME } from '../constants/theme.constants.js';
|
||||
import { sequelize } from '../../../../shared/database/models.js';
|
||||
import { AuditService } from '../../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class ThemeService {
|
||||
async getCurrentTheme(userId) {
|
||||
if (!userId) {
|
||||
return { themeCode: DEFAULT_THEME };
|
||||
}
|
||||
const preference = await repository.findByUserId(userId);
|
||||
return {
|
||||
themeCode: preference ? preference.theme_code : DEFAULT_THEME
|
||||
};
|
||||
}
|
||||
|
||||
validateTheme(themeCode) {
|
||||
if (!ALLOWED_THEME_CODES.includes(themeCode)) {
|
||||
throw new ApiError(400, `Invalid theme code: "${themeCode}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateCurrentTheme(userId, themeCode, requestContext = {}) {
|
||||
if (!userId) {
|
||||
throw new ApiError(401, 'Unauthorized');
|
||||
}
|
||||
|
||||
this.validateTheme(themeCode);
|
||||
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const existing = await repository.findByUserId(userId, { transaction });
|
||||
const oldTheme = existing ? existing.theme_code : DEFAULT_THEME;
|
||||
|
||||
let result;
|
||||
if (existing) {
|
||||
result = await existing.update({ theme_code: themeCode }, { transaction });
|
||||
} else {
|
||||
result = await repository.create({
|
||||
user_id: userId,
|
||||
theme_code: themeCode
|
||||
}, { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// Log the theme change via the existing Audit System.
|
||||
// We pass a dummy 'method' in details to satisfy AuditService validation checks.
|
||||
await AuditService.log({
|
||||
action: 'UPDATE_THEME_PREFERENCE',
|
||||
resource: 'settings',
|
||||
resourceId: result.id,
|
||||
userId: userId,
|
||||
old_value: { theme_code: oldTheme },
|
||||
new_value: { theme_code: themeCode },
|
||||
details: {
|
||||
method: requestContext.method || 'PUT',
|
||||
ip: requestContext.ip || '127.0.0.1',
|
||||
userAgent: requestContext.userAgent || 'system'
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ThemeService();
|
||||
@@ -0,0 +1,12 @@
|
||||
import { body } from 'express-validator';
|
||||
import { ALLOWED_THEME_CODES } from '../constants/theme.constants.js';
|
||||
|
||||
export const themeValidation = [
|
||||
body('themeCode')
|
||||
.notEmpty()
|
||||
.withMessage('themeCode is required')
|
||||
.isString()
|
||||
.withMessage('themeCode must be a string')
|
||||
.isIn(ALLOWED_THEME_CODES)
|
||||
.withMessage(`themeCode must be one of: ${ALLOWED_THEME_CODES.join(', ')}`)
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('user_themes', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
theme_code: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('user_themes', ['user_id']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('user_themes');
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,10 @@ export const associateModels = () => {
|
||||
});
|
||||
|
||||
// Explicit RBAC Associations
|
||||
if (models.User && models.UserTheme) {
|
||||
models.User.hasOne(models.UserTheme, { foreignKey: 'user_id', as: 'theme' });
|
||||
}
|
||||
|
||||
if (models.Tenant && models.User) {
|
||||
models.User.belongsTo(models.Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
||||
models.Tenant.hasMany(models.User, { foreignKey: 'tenant_id', as: 'users' });
|
||||
@@ -129,6 +133,7 @@ import brandModelInit from '../../features/brands/brands/brand.model.js';
|
||||
import unitModelInit from '../../features/brands/units/unit.model.js';
|
||||
import settingModelInit from '../../features/settings/settings/setting.model.js';
|
||||
import auditLogModelInit from '../../features/auditLogs/auditLogs/auditLog.model.js';
|
||||
import userThemeModelInit from '../../features/settings/theme/models/userTheme.model.js';
|
||||
|
||||
// Attribute Management Models
|
||||
import attributeModelInit from '../../features/attributes/attributes/attribute.model.js';
|
||||
@@ -191,6 +196,7 @@ export const initializeDatabaseModels = () => {
|
||||
registerModel('Unit', unitModelInit);
|
||||
registerModel('Setting', settingModelInit);
|
||||
registerModel('AuditLog', auditLogModelInit);
|
||||
registerModel('UserTheme', userThemeModelInit);
|
||||
|
||||
// Attribute Management
|
||||
registerModel('Attribute', attributeModelInit);
|
||||
|
||||
Reference in New Issue
Block a user