Skip to content

Commit 0e492f4

Browse files
authored
Merge pull request #16 from foyzulkarim/feature/rbac/role-crud
Add Role Management API Routes
2 parents 7beb0a9 + 8e97381 commit 0e492f4

File tree

7 files changed

+338
-0
lines changed

7 files changed

+338
-0
lines changed

src/domains/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ const productRoutes = require('./product');
22
const userRoutes = require('./user');
33
const repositoryRoutes = require('./repository');
44
const prRoutes = require('./pull');
5+
const roleRoutes = require('./role');
56

67
const defineRoutes = async (expressRouter) => {
78
productRoutes(expressRouter);
89
userRoutes(expressRouter);
910
repositoryRoutes(expressRouter);
1011
prRoutes(expressRouter);
12+
roleRoutes(expressRouter);
1113
};
1214

1315
module.exports = defineRoutes;

src/domains/role/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 = 'Role';
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/role/event.js

Whitespace-only changes.

src/domains/role/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('/roles', routes());
5+
};
6+
7+
module.exports = defineRoutes;

src/domains/role/request.js

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

src/domains/role/schema.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const mongoose = require('mongoose');
2+
const { baseSchema } = require('../../libraries/db/base-schema');
3+
4+
const schema = 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+
permissions: [{
21+
type: mongoose.Schema.Types.ObjectId,
22+
ref: 'Permission'
23+
}],
24+
isSystem: {
25+
type: Boolean,
26+
default: false // To mark system-level roles like 'superadmin'
27+
}
28+
});
29+
30+
schema.add(baseSchema);
31+
32+
module.exports = mongoose.model('Role', schema);

src/domains/role/service.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const logger = require('../../libraries/log/logger');
2+
const Model = require('./schema');
3+
const { AppError } = require('../../libraries/error-handling/AppError');
4+
5+
const model = 'role';
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+
} = query ?? {};
31+
32+
const filter = {};
33+
if (keyword) {
34+
filter.$or = [
35+
{ name: { $regex: keyword, $options: 'i' } },
36+
{ displayName: { $regex: keyword, $options: 'i' } },
37+
];
38+
}
39+
40+
const items = await Model.find(filter)
41+
.sort({ [orderBy]: order === 'asc' ? 1 : -1 })
42+
.skip(page * pageSize)
43+
.limit(pageSize);
44+
45+
logger.info('search(): filter and count', {
46+
filter,
47+
count: items.length,
48+
});
49+
return items;
50+
} catch (error) {
51+
logger.error(`search(): Failed to search ${model}`, error);
52+
throw new AppError(`Failed to search ${model}`, error.message, 400);
53+
}
54+
};
55+
56+
const count = async (query) => {
57+
try {
58+
const { keyword } = query ?? {};
59+
const filter = {};
60+
if (keyword) {
61+
filter.$or = [
62+
{ name: { $regex: keyword, $options: 'i' } },
63+
{ displayName: { $regex: keyword, $options: 'i' } },
64+
];
65+
}
66+
const total = await Model.countDocuments(filter);
67+
logger.info('count(): filter and count', {
68+
filter,
69+
count: total,
70+
});
71+
return total;
72+
} catch (error) {
73+
logger.error(`count(): Failed to count ${model}`, error);
74+
throw new AppError(`Failed to count ${model}`, error.message, 400);
75+
}
76+
};
77+
78+
const getById = async (id) => {
79+
try {
80+
const item = await Model.findById(id);
81+
logger.info(`getById(): ${model} fetched`, { id });
82+
return item;
83+
} catch (error) {
84+
logger.error(`getById(): Failed to get ${model}`, error);
85+
throw new AppError(`Failed to get ${model}`, error.message);
86+
}
87+
};
88+
89+
const updateById = async (id, data) => {
90+
try {
91+
const item = await Model.findByIdAndUpdate(id, data, { new: true });
92+
logger.info(`updateById(): ${model} updated`, { id });
93+
return item;
94+
} catch (error) {
95+
logger.error(`updateById(): Failed to update ${model}`, error);
96+
throw new AppError(`Failed to update ${model}`, error.message);
97+
}
98+
};
99+
100+
const deleteById = async (id) => {
101+
try {
102+
await Model.findByIdAndDelete(id);
103+
logger.info(`deleteById(): ${model} deleted`, { id });
104+
return true;
105+
} catch (error) {
106+
logger.error(`deleteById(): Failed to delete ${model}`, error);
107+
throw new AppError(`Failed to delete ${model}`, error.message);
108+
}
109+
};
110+
111+
module.exports = {
112+
create,
113+
search,
114+
count,
115+
getById,
116+
updateById,
117+
deleteById,
118+
};

0 commit comments

Comments
 (0)