Last git push by Hasan ( create the table in DB for sava user Theme)

This commit is contained in:
MohamedHasan07
2026-07-28 11:05:47 +05:30
parent 4e7d9b1ee1
commit 64afe7dc00
12 changed files with 317 additions and 7 deletions
+2
View File
@@ -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();
+5
View File
@@ -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');
}
};
+6
View File
@@ -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);