Skip to content

Commit 95730d5

Browse files
authored
Merge pull request #17 from foyzulkarim/feature/rbac/resource-management
Role and Resource Management Implementation
2 parents 0e492f4 + 7914490 commit 95730d5

File tree

11 files changed

+472
-9
lines changed

11 files changed

+472
-9
lines changed

src/domains/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ const userRoutes = require('./user');
33
const repositoryRoutes = require('./repository');
44
const prRoutes = require('./pull');
55
const roleRoutes = require('./role');
6+
const resourceRoutes = require('./resource');
67

78
const defineRoutes = async (expressRouter) => {
89
productRoutes(expressRouter);
910
userRoutes(expressRouter);
1011
repositoryRoutes(expressRouter);
1112
prRoutes(expressRouter);
1213
roleRoutes(expressRouter);
14+
resourceRoutes(expressRouter);
1315
};
1416

1517
module.exports = defineRoutes;

src/domains/resource/api.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const express = require('express');
2+
const logger = require('../../libraries/log/logger');
3+
const { AppError } = require('../../libraries/error-handling/AppError');
4+
5+
const {
6+
create,
7+
search,
8+
count,
9+
getById,
10+
updateById,
11+
deleteById,
12+
} = require('./service');
13+
14+
const {
15+
createSchema,
16+
updateSchema,
17+
idSchema,
18+
searchSchema,
19+
} = require('./request');
20+
const { validateRequest } = require('../../middlewares/request-validate');
21+
const { logRequest } = require('../../middlewares/log');
22+
const { isAuthorized } = require('../../middlewares/auth/authorization');
23+
24+
const model = 'Resource';
25+
26+
const routes = () => {
27+
const router = express.Router();
28+
logger.info(`Setting up routes for ${model}`);
29+
30+
router.get(
31+
'/search',
32+
logRequest({}),
33+
validateRequest({ schema: searchSchema, isQuery: true }),
34+
async (req, res, next) => {
35+
try {
36+
const items = await search(req.query);
37+
res.json(items);
38+
} catch (error) {
39+
next(error);
40+
}
41+
}
42+
);
43+
44+
router.get(
45+
'/count',
46+
logRequest({}),
47+
validateRequest({ schema: searchSchema, isQuery: true }),
48+
async (req, res, next) => {
49+
try {
50+
const total = await count(req.query);
51+
res.json({ total });
52+
} catch (error) {
53+
next(error);
54+
}
55+
}
56+
);
57+
58+
router.post(
59+
'/',
60+
logRequest({}),
61+
isAuthorized,
62+
validateRequest({ schema: createSchema }),
63+
async (req, res, next) => {
64+
try {
65+
const item = await create(req.body);
66+
res.status(201).json(item);
67+
} catch (error) {
68+
next(error);
69+
}
70+
}
71+
);
72+
73+
router.get(
74+
'/:id',
75+
logRequest({}),
76+
validateRequest({ schema: idSchema, isParam: true }),
77+
async (req, res, next) => {
78+
try {
79+
const item = await getById(req.params.id);
80+
if (!item) {
81+
throw new AppError(`${model} not found`, `${model} not found`, 404);
82+
}
83+
res.status(200).json(item);
84+
} catch (error) {
85+
next(error);
86+
}
87+
}
88+
);
89+
90+
router.put(
91+
'/:id',
92+
logRequest({}),
93+
isAuthorized,
94+
validateRequest({ schema: idSchema, isParam: true }),
95+
validateRequest({ schema: updateSchema }),
96+
async (req, res, next) => {
97+
try {
98+
const item = await updateById(req.params.id, req.body);
99+
if (!item) {
100+
throw new AppError(`${model} not found`, `${model} not found`, 404);
101+
}
102+
res.status(200).json(item);
103+
} catch (error) {
104+
next(error);
105+
}
106+
}
107+
);
108+
109+
router.delete(
110+
'/:id',
111+
logRequest({}),
112+
isAuthorized,
113+
validateRequest({ schema: idSchema, isParam: true }),
114+
async (req, res, next) => {
115+
try {
116+
await deleteById(req.params.id);
117+
res.status(204).json({ message: `${model} is deleted` });
118+
} catch (error) {
119+
next(error);
120+
}
121+
}
122+
);
123+
124+
return router;
125+
};
126+
127+
module.exports = { routes };

src/domains/resource/event.js

Whitespace-only changes.

src/domains/resource/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { routes } = require('./api');
2+
3+
const defineRoutes = (expressRouter) => {
4+
expressRouter.use('/resources', routes());
5+
};
6+
7+
module.exports = defineRoutes;

src/domains/resource/request.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const Joi = require('joi');
2+
const mongoose = require('mongoose');
3+
4+
const createSchema = Joi.object().keys({
5+
name: Joi.string().required().lowercase().trim(),
6+
displayName: Joi.string().required(),
7+
description: Joi.string().allow('').optional(),
8+
identifier: Joi.string().required(),
9+
type: Joi.string().valid('api', 'ui', 'menu').default('api')
10+
});
11+
12+
const updateSchema = Joi.object().keys({
13+
name: Joi.string().lowercase().trim(),
14+
displayName: Joi.string(),
15+
description: Joi.string().allow(''),
16+
identifier: Joi.string(),
17+
type: Joi.string().valid('api', 'ui', 'menu')
18+
});
19+
20+
const idSchema = Joi.object().keys({
21+
id: Joi.string()
22+
.custom((value, helpers) => {
23+
if (!mongoose.Types.ObjectId.isValid(value)) {
24+
return helpers.error('any.invalid');
25+
}
26+
return value;
27+
}, 'ObjectId validation')
28+
.required(),
29+
});
30+
31+
const searchSchema = Joi.object({
32+
keyword: Joi.string().allow('').optional().max(50),
33+
page: Joi.number().integer().min(0),
34+
orderBy: Joi.string(),
35+
order: Joi.string().valid('asc', 'desc'),
36+
type: Joi.string().valid('api', 'ui', 'menu')
37+
});
38+
39+
module.exports = { createSchema, updateSchema, idSchema, searchSchema };

src/domains/resource/schema.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const mongoose = require('mongoose');
2+
const { baseSchema } = require('../../libraries/db/base-schema');
3+
4+
const resourceSchema = new mongoose.Schema({
5+
name: {
6+
type: String,
7+
required: true,
8+
unique: true,
9+
lowercase: true,
10+
trim: true,
11+
},
12+
displayName: {
13+
type: String,
14+
required: true,
15+
},
16+
description: {
17+
type: String,
18+
default: '',
19+
},
20+
identifier: {
21+
type: String,
22+
required: true,
23+
unique: true,
24+
},
25+
type: {
26+
type: String,
27+
enum: ['api', 'ui', 'menu'],
28+
default: 'api',
29+
},
30+
});
31+
32+
resourceSchema.add(baseSchema);
33+
34+
module.exports = mongoose.model('Resource', resourceSchema);

src/domains/resource/service.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
const logger = require('../../libraries/log/logger');
2+
const Model = require('./schema');
3+
const { AppError } = require('../../libraries/error-handling/AppError');
4+
5+
const model = 'resource';
6+
7+
const create = async (data) => {
8+
try {
9+
const item = new Model(data);
10+
const saved = await item.save();
11+
logger.info(`create(): ${model} created`, {
12+
id: saved._id,
13+
});
14+
return saved;
15+
} catch (error) {
16+
logger.error(`create(): Failed to create ${model}`, error);
17+
throw new AppError(`Failed to create ${model}`, error.message);
18+
}
19+
};
20+
21+
const search = async (query) => {
22+
try {
23+
logger.info(`search(): ${model} search`, { query });
24+
const pageSize = 10;
25+
const {
26+
keyword,
27+
page = 0,
28+
orderBy = 'name',
29+
order = 'asc',
30+
type
31+
} = query ?? {};
32+
33+
const filter = {};
34+
if (keyword) {
35+
filter.$or = [
36+
{ name: { $regex: keyword, $options: 'i' } },
37+
{ displayName: { $regex: keyword, $options: 'i' } },
38+
{ identifier: { $regex: keyword, $options: 'i' } }
39+
];
40+
}
41+
if (type) {
42+
filter.type = type;
43+
}
44+
45+
const items = await Model.find(filter)
46+
.sort({ [orderBy]: order === 'asc' ? 1 : -1 })
47+
.skip(page * pageSize)
48+
.limit(pageSize);
49+
50+
logger.info('search(): filter and count', {
51+
filter,
52+
count: items.length,
53+
});
54+
return items;
55+
} catch (error) {
56+
logger.error(`search(): Failed to search ${model}`, error);
57+
throw new AppError(`Failed to search ${model}`, error.message, 400);
58+
}
59+
};
60+
61+
const count = async (query) => {
62+
try {
63+
const { keyword, type } = query ?? {};
64+
const filter = {};
65+
if (keyword) {
66+
filter.$or = [
67+
{ name: { $regex: keyword, $options: 'i' } },
68+
{ displayName: { $regex: keyword, $options: 'i' } },
69+
{ identifier: { $regex: keyword, $options: 'i' } }
70+
];
71+
}
72+
if (type) {
73+
filter.type = type;
74+
}
75+
const total = await Model.countDocuments(filter);
76+
logger.info('count(): filter and count', {
77+
filter,
78+
count: total,
79+
});
80+
return total;
81+
} catch (error) {
82+
logger.error(`count(): Failed to count ${model}`, error);
83+
throw new AppError(`Failed to count ${model}`, error.message, 400);
84+
}
85+
};
86+
87+
const getById = async (id) => {
88+
try {
89+
const item = await Model.findById(id);
90+
logger.info(`getById(): ${model} fetched`, { id });
91+
return item;
92+
} catch (error) {
93+
logger.error(`getById(): Failed to get ${model}`, error);
94+
throw new AppError(`Failed to get ${model}`, error.message);
95+
}
96+
};
97+
98+
const updateById = async (id, data) => {
99+
try {
100+
const item = await Model.findByIdAndUpdate(id, data, { new: true });
101+
logger.info(`updateById(): ${model} updated`, { id });
102+
return item;
103+
} catch (error) {
104+
logger.error(`updateById(): Failed to update ${model}`, error);
105+
throw new AppError(`Failed to update ${model}`, error.message);
106+
}
107+
};
108+
109+
const deleteById = async (id) => {
110+
try {
111+
await Model.findByIdAndDelete(id);
112+
logger.info(`deleteById(): ${model} deleted`, { id });
113+
return true;
114+
} catch (error) {
115+
logger.error(`deleteById(): Failed to delete ${model}`, error);
116+
throw new AppError(`Failed to delete ${model}`, error.message);
117+
}
118+
};
119+
120+
const getAllGroupedByType = async () => {
121+
try {
122+
const resources = await Model.find({})
123+
.sort({ type: 1, displayName: 1 })
124+
.lean();
125+
126+
// Group resources by type
127+
const grouped = resources.reduce((acc, resource) => {
128+
if (!acc[resource.type]) {
129+
acc[resource.type] = [];
130+
}
131+
acc[resource.type].push({
132+
id: resource._id,
133+
name: resource.name,
134+
displayName: resource.displayName,
135+
description: resource.description,
136+
identifier: resource.identifier
137+
});
138+
return acc;
139+
}, {});
140+
141+
logger.info('getAllGroupedByType(): Resources fetched and grouped');
142+
return grouped;
143+
} catch (error) {
144+
logger.error('getAllGroupedByType(): Failed to fetch resources', error);
145+
throw new AppError('Failed to fetch resources', error.message, 400);
146+
}
147+
};
148+
149+
module.exports = {
150+
create,
151+
search,
152+
count,
153+
getById,
154+
updateById,
155+
deleteById,
156+
getAllGroupedByType,
157+
};

0 commit comments

Comments
 (0)