@yunsoft/yuncms-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import express from 'express';
3
+
4
+ import { discoverExtensions } from './discovery.js';
5
+
6
+ function extensionError(code, message) {
7
+ const error = new Error(message);
8
+ error.code = code;
9
+ return error;
10
+ }
11
+
12
+ function assertDefinition(definition, manifest) {
13
+ if (!definition || definition.__yuncms_extension__ !== true || typeof definition.register !== 'function') {
14
+ throw extensionError(
15
+ 'INVALID_EXTENSION_DEFINITION',
16
+ `Extension ${manifest.id} must default-export a YunCMS extension definition`,
17
+ );
18
+ }
19
+ if (definition.type !== manifest.type) {
20
+ throw extensionError(
21
+ 'EXTENSION_TYPE_MISMATCH',
22
+ `Extension ${manifest.id} manifest type ${manifest.type} does not match definition type ${definition.type}`,
23
+ );
24
+ }
25
+ return definition;
26
+ }
27
+
28
+ async function importExtension(manifest) {
29
+ const moduleUrl = pathToFileURL(manifest.entry).href;
30
+ const imported = await import(moduleUrl);
31
+ return assertDefinition(imported.default, manifest);
32
+ }
33
+
34
+ function createBaseContext({ services, database, schemaCache, emitter, storage, logger, env }) {
35
+ return Object.freeze({
36
+ services,
37
+ database,
38
+ logger,
39
+ env,
40
+ emitter,
41
+ storage,
42
+ getSchema: () => schemaCache.get(database),
43
+ getAccountability: (req) => req?.accountability ?? null,
44
+ serviceOptions: async (req) => ({
45
+ accountability: req?.accountability,
46
+ database,
47
+ schema: req?.context?.schema ?? await schemaCache.get(database),
48
+ logger,
49
+ emitter,
50
+ storage,
51
+ permissionCache: req?.context?.permissionCache ?? new Map(),
52
+ requestId: req?.id ?? null,
53
+ }),
54
+ });
55
+ }
56
+
57
+ function hookRegistrationApi(emitter, baseContext) {
58
+ return Object.freeze({
59
+ filter(event, handler) {
60
+ return emitter.registerFilter(event, (payload, eventContext) =>
61
+ handler(payload, { ...baseContext, ...eventContext }));
62
+ },
63
+ action(event, handler) {
64
+ return emitter.registerAction(event, (payload, eventContext) =>
65
+ handler(payload, { ...baseContext, ...eventContext }));
66
+ },
67
+ init(event, handler) {
68
+ return emitter.registerInit(event, (eventContext) =>
69
+ handler({ ...baseContext, ...eventContext }));
70
+ },
71
+ });
72
+ }
73
+
74
+ export async function loadExtensionRuntime({
75
+ rootDir = process.cwd(),
76
+ localDirectory = 'extensions',
77
+ includeDependencies = true,
78
+ services,
79
+ database,
80
+ schemaCache,
81
+ emitter,
82
+ storage = null,
83
+ logger = console,
84
+ env,
85
+ } = {}) {
86
+ if (!services || !database || !schemaCache || !emitter) {
87
+ throw new Error('Extension runtime requires services, database, schemaCache and emitter');
88
+ }
89
+
90
+ const manifests = await discoverExtensions({ rootDir, localDirectory, includeDependencies });
91
+ const baseContext = createBaseContext({ services, database, schemaCache, emitter, storage, logger, env });
92
+ const endpointExtensions = [];
93
+ const hookApi = hookRegistrationApi(emitter, baseContext);
94
+
95
+ for (const manifest of manifests) {
96
+ const definition = await importExtension(manifest);
97
+
98
+ if (manifest.type === 'hook') {
99
+ await definition.register(hookApi, baseContext);
100
+ logger.info?.(`Loaded YunCMS hook extension: ${manifest.id}`);
101
+ continue;
102
+ }
103
+
104
+ const router = express.Router();
105
+ await definition.register(router, baseContext);
106
+ endpointExtensions.push(Object.freeze({
107
+ id: manifest.id,
108
+ router,
109
+ manifest,
110
+ }));
111
+ logger.info?.(`Loaded YunCMS endpoint extension: ${manifest.id}`);
112
+ }
113
+
114
+ return Object.freeze({
115
+ manifests: Object.freeze(manifests),
116
+ endpointExtensions: Object.freeze(endpointExtensions),
117
+ async init(event) {
118
+ await emitter.init(event, baseContext);
119
+ },
120
+ });
121
+ }
@@ -0,0 +1,54 @@
1
+ function rateLimitError(retryAfterSeconds) {
2
+ const error = new Error('Too many requests');
3
+ error.code = 'RATE_LIMITED';
4
+ error.retryAfterSeconds = retryAfterSeconds;
5
+ return error;
6
+ }
7
+
8
+ export function createFixedWindowRateLimit({
9
+ windowMs,
10
+ max,
11
+ key = (req) => req.ip || req.socket?.remoteAddress || 'unknown',
12
+ maxBuckets = 10_000,
13
+ now = () => Date.now(),
14
+ } = {}) {
15
+ if (!Number.isInteger(windowMs) || windowMs < 1000) throw new Error('Rate-limit windowMs must be at least 1000');
16
+ if (!Number.isInteger(max) || max < 1) throw new Error('Rate-limit max must be a positive integer');
17
+
18
+ const buckets = new Map();
19
+
20
+ function prune(timestamp) {
21
+ if (buckets.size < maxBuckets) return;
22
+ for (const [bucketKey, bucket] of buckets) {
23
+ if (bucket.resetAt <= timestamp) buckets.delete(bucketKey);
24
+ if (buckets.size < maxBuckets) break;
25
+ }
26
+ }
27
+
28
+ return (req, res, next) => {
29
+ const timestamp = now();
30
+ prune(timestamp);
31
+ const bucketKey = String(key(req));
32
+ let bucket = buckets.get(bucketKey);
33
+
34
+ if (!bucket || bucket.resetAt <= timestamp) {
35
+ bucket = { count: 0, resetAt: timestamp + windowMs };
36
+ buckets.set(bucketKey, bucket);
37
+ }
38
+
39
+ bucket.count += 1;
40
+ const remaining = Math.max(0, max - bucket.count);
41
+ const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - timestamp) / 1000));
42
+ res.set('x-ratelimit-limit', String(max));
43
+ res.set('x-ratelimit-remaining', String(remaining));
44
+ res.set('x-ratelimit-reset', String(Math.ceil(bucket.resetAt / 1000)));
45
+
46
+ if (bucket.count > max) {
47
+ res.set('retry-after', String(retryAfterSeconds));
48
+ next(rateLimitError(retryAfterSeconds));
49
+ return;
50
+ }
51
+
52
+ next();
53
+ };
54
+ }
@@ -0,0 +1,45 @@
1
+ import express from 'express';
2
+
3
+ import { serviceOptionsFromRequest } from '../service-options.js';
4
+
5
+ function auditService(req) {
6
+ const Service = req.context.services.AuditService;
7
+ return new Service(serviceOptionsFromRequest(req));
8
+ }
9
+
10
+ function integerQuery(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
11
+ if (value == null || value === '') return fallback;
12
+ const parsed = Number(value);
13
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
14
+ const error = new Error('Invalid audit pagination value');
15
+ error.code = 'INVALID_QUERY';
16
+ throw error;
17
+ }
18
+ return parsed;
19
+ }
20
+
21
+ export function createAuditRouter() {
22
+ const router = express.Router();
23
+
24
+ router.get('/', async (req, res) => {
25
+ const data = await auditService(req).readMany({
26
+ limit: integerQuery(req.query.limit, 100, { min: 1, max: 500 }),
27
+ offset: integerQuery(req.query.offset, 0, { min: 0 }),
28
+ collection: req.query.collection || null,
29
+ user: req.query.user || null,
30
+ });
31
+ res.json({ data });
32
+ });
33
+
34
+ router.post('/cleanup', async (req, res) => {
35
+ const defaults = req.context.env?.audit ?? {};
36
+ const data = await auditService(req).cleanup({
37
+ retentionDays: req.body?.retentionDays ?? defaults.retentionDays,
38
+ batchSize: req.body?.batchSize ?? defaults.cleanupBatchSize,
39
+ maxBatches: req.body?.maxBatches ?? defaults.cleanupMaxBatches,
40
+ });
41
+ res.json({ data });
42
+ });
43
+
44
+ return router;
45
+ }
@@ -0,0 +1,179 @@
1
+ import express from 'express';
2
+
3
+ import { createFixedWindowRateLimit } from '../rate-limit.js';
4
+ import { serviceOptionsFromRequest } from '../service-options.js';
5
+
6
+ function service(req, name) {
7
+ const Service = req.context.services[name];
8
+ return new Service(serviceOptionsFromRequest(req));
9
+ }
10
+
11
+ function authService(req) {
12
+ return service(req, 'AuthService');
13
+ }
14
+
15
+ function authTokensService(req) {
16
+ return service(req, 'AuthTokensService');
17
+ }
18
+
19
+ function apiTokensService(req) {
20
+ return service(req, 'ApiTokensService');
21
+ }
22
+
23
+ function usersService(req) {
24
+ return service(req, 'UsersService');
25
+ }
26
+
27
+ function requireSessionAuthentication(req) {
28
+ if (req.authMethod !== 'session' || !req.authToken) {
29
+ const error = new Error('Session access token is required');
30
+ error.code = 'UNAUTHORIZED';
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ function requireMailer(mailer) {
36
+ if (mailer) return mailer;
37
+ const error = new Error('SMTP mail delivery is not configured');
38
+ error.code = 'MAIL_NOT_CONFIGURED';
39
+ throw error;
40
+ }
41
+
42
+ function actionUrl(config, action, token) {
43
+ return `${config.auth.publicUrl}/?auth_action=${encodeURIComponent(action)}&token=${encodeURIComponent(token)}`;
44
+ }
45
+
46
+ function noStore(req, res, next) {
47
+ res.set('cache-control', 'no-store');
48
+ res.set('pragma', 'no-cache');
49
+ next();
50
+ }
51
+
52
+ export function createAuthRouter({ mailer = null, config = null, logger = console } = {}) {
53
+ const router = express.Router();
54
+ const limits = config?.auth?.rateLimit ?? {};
55
+ const loginLimit = createFixedWindowRateLimit({
56
+ windowMs: limits.loginWindowMs ?? 60_000,
57
+ max: limits.loginMax ?? 10,
58
+ });
59
+ const refreshLimit = createFixedWindowRateLimit({
60
+ windowMs: limits.refreshWindowMs ?? 60_000,
61
+ max: limits.refreshMax ?? 30,
62
+ });
63
+ const actionLimit = createFixedWindowRateLimit({
64
+ windowMs: limits.actionWindowMs ?? 15 * 60_000,
65
+ max: limits.actionMax ?? 5,
66
+ });
67
+
68
+ router.use(noStore);
69
+
70
+ router.post('/login', loginLimit, async (req, res) => {
71
+ const result = await authService(req).login({
72
+ email: req.body?.email,
73
+ password: req.body?.password,
74
+ ip: req.ip ?? null,
75
+ userAgent: req.get('user-agent') ?? null,
76
+ });
77
+ res.json({ data: result });
78
+ });
79
+
80
+ router.post('/refresh', refreshLimit, async (req, res) => {
81
+ const result = await authService(req).refresh(req.body?.refresh_token);
82
+ res.json({ data: result });
83
+ });
84
+
85
+ router.post('/logout', async (req, res) => {
86
+ requireSessionAuthentication(req);
87
+ await authService(req).logout(req.authToken);
88
+ res.status(204).end();
89
+ });
90
+
91
+ router.post('/logout-all', async (req, res) => {
92
+ requireSessionAuthentication(req);
93
+ await authService(req).logoutAll();
94
+ res.status(204).end();
95
+ });
96
+
97
+ router.post('/password-reset/request', actionLimit, async (req, res) => {
98
+ const transport = requireMailer(mailer);
99
+ const result = await authTokensService(req).requestPasswordReset(req.body?.email);
100
+
101
+ if (result) {
102
+ try {
103
+ const url = actionUrl(config, 'reset', result.token);
104
+ await transport.send({
105
+ to: String(req.body.email).trim(),
106
+ subject: 'Reset your YunCMS password',
107
+ text: `A password reset was requested for your YunCMS account.\n\nOpen this link to choose a new password:\n${url}\n\nIf you did not request this, you can ignore this message.`,
108
+ });
109
+ } catch (error) {
110
+ logger.error?.('YunCMS password reset mail delivery failed', {
111
+ requestId: req.id,
112
+ code: error?.code,
113
+ message: error?.message,
114
+ });
115
+ }
116
+ }
117
+
118
+ res.status(202).json({ data: { accepted: true } });
119
+ });
120
+
121
+ router.post('/password-reset/confirm', actionLimit, async (req, res) => {
122
+ await authTokensService(req).resetPassword(req.body?.token, req.body?.password);
123
+ res.status(204).end();
124
+ });
125
+
126
+ router.post('/email-verification/request', actionLimit, async (req, res) => {
127
+ const transport = requireMailer(mailer);
128
+ if (!req.accountability?.user) {
129
+ const error = new Error('Authentication is required to request email verification');
130
+ error.code = 'UNAUTHORIZED';
131
+ throw error;
132
+ }
133
+
134
+ const userId = req.body?.user ?? req.accountability.user;
135
+ const user = await usersService(req).readOne(userId);
136
+ if (!user) {
137
+ const error = new Error(`User not found: ${userId}`);
138
+ error.code = 'USER_NOT_FOUND';
139
+ throw error;
140
+ }
141
+ const result = await authTokensService(req).createEmailVerification(userId);
142
+ const url = actionUrl(config, 'verify', result.token);
143
+ await transport.send({
144
+ to: user.email,
145
+ subject: 'Verify your YunCMS email',
146
+ text: `Verify your YunCMS email address by opening this link:\n${url}\n\nIf you did not request this, you can ignore this message.`,
147
+ });
148
+ res.status(202).json({ data: { accepted: true } });
149
+ });
150
+
151
+ router.post('/email-verification/confirm', actionLimit, async (req, res) => {
152
+ await authTokensService(req).verifyEmail(req.body?.token);
153
+ res.status(204).end();
154
+ });
155
+
156
+ router.get('/tokens', async (req, res) => {
157
+ const data = await apiTokensService(req).readMany();
158
+ res.json({ data });
159
+ });
160
+
161
+ router.post('/tokens', async (req, res) => {
162
+ const data = await apiTokensService(req).createOne(req.body ?? {});
163
+ res.status(201).json({ data });
164
+ });
165
+
166
+ router.delete('/tokens/:id', async (req, res) => {
167
+ const deleted = await apiTokensService(req).deleteOne(req.params.id);
168
+ if (!deleted) {
169
+ const error = new Error('API token not found');
170
+ error.code = 'NOT_FOUND';
171
+ throw error;
172
+ }
173
+ res.status(204).end();
174
+ });
175
+
176
+ return router;
177
+ }
178
+
179
+ export { actionUrl, noStore, requireMailer, requireSessionAuthentication };
@@ -0,0 +1,98 @@
1
+ import express from 'express';
2
+
3
+ import { serviceOptionsFromRequest } from '../service-options.js';
4
+
5
+ function filesService(req) {
6
+ const Service = req.context.services.FilesService;
7
+ return new Service(serviceOptionsFromRequest(req));
8
+ }
9
+
10
+ function reconciliationService(req) {
11
+ const Service = req.context.services.FileReconciliationService;
12
+ return new Service(serviceOptionsFromRequest(req));
13
+ }
14
+
15
+ function notFound(id) {
16
+ const error = new Error(`File not found: ${id}`);
17
+ error.code = 'FILE_NOT_FOUND';
18
+ return error;
19
+ }
20
+
21
+ function decodeFilenameHeader(value) {
22
+ if (!value) return value;
23
+ try {
24
+ return decodeURIComponent(value);
25
+ } catch {
26
+ const error = new Error('Encoded upload filename is invalid');
27
+ error.code = 'INVALID_PAYLOAD';
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function attachmentHeader(filename) {
33
+ const safe = String(filename || 'download')
34
+ .replace(/[\r\n]/g, '')
35
+ .replace(/["\\]/g, '_');
36
+ return `attachment; filename="${safe.replace(/[^\x20-\x7e]/g, '_')}"; filename*=UTF-8''${encodeURIComponent(safe)}`;
37
+ }
38
+
39
+ export function createFilesRouter({ maxUploadBytes = 25 * 1024 * 1024 } = {}) {
40
+ const router = express.Router();
41
+ const rawUpload = express.raw({ type: 'application/octet-stream', limit: maxUploadBytes });
42
+
43
+ router.get('/', async (req, res) => {
44
+ res.json({ data: await filesService(req).readMany() });
45
+ });
46
+
47
+ router.post('/', rawUpload, async (req, res) => {
48
+ const filenameDownload = decodeFilenameHeader(req.get('x-filename'));
49
+ const title = req.get('x-title') || null;
50
+ const mimetype = req.get('x-mimetype') || 'application/octet-stream';
51
+ const storage = req.query.storage || 'local';
52
+ const data = await filesService(req).createOne({
53
+ contents: req.body,
54
+ filenameDownload,
55
+ title,
56
+ mimetype,
57
+ storage,
58
+ });
59
+ res.status(201).json({ data });
60
+ });
61
+
62
+ router.post('/reconcile', async (req, res) => {
63
+ const data = await reconciliationService(req).scan({
64
+ storage: req.body?.storage ?? 'local',
65
+ deleteOrphans: req.body?.deleteOrphans === true,
66
+ minimumAgeMs: req.body?.minimumAgeMs,
67
+ });
68
+ res.json({ data });
69
+ });
70
+
71
+ router.get('/:id', async (req, res) => {
72
+ const data = await filesService(req).readOne(req.params.id);
73
+ if (!data) throw notFound(req.params.id);
74
+ res.json({ data });
75
+ });
76
+
77
+ router.get('/:id/content', async (req, res) => {
78
+ const result = await filesService(req).readContent(req.params.id);
79
+ res.set('content-type', result.file.mimetype || 'application/octet-stream');
80
+ res.set('content-length', String(result.contents.byteLength));
81
+ res.set('content-disposition', attachmentHeader(result.file.filename_download));
82
+ res.send(result.contents);
83
+ });
84
+
85
+ router.patch('/:id', async (req, res) => {
86
+ const data = await filesService(req).updateOne(req.params.id, req.body ?? {});
87
+ res.json({ data });
88
+ });
89
+
90
+ router.delete('/:id', async (req, res) => {
91
+ await filesService(req).deleteOne(req.params.id);
92
+ res.status(204).end();
93
+ });
94
+
95
+ return router;
96
+ }
97
+
98
+ export { attachmentHeader, decodeFilenameHeader };
@@ -0,0 +1,75 @@
1
+ import express from 'express';
2
+ import { readManyWithRelations, readOneWithRelations } from '@yunsoft/yuncms-core';
3
+
4
+ import { serviceOptionsFromRequest } from '../service-options.js';
5
+
6
+ export function createItemsRouter() {
7
+ const router = express.Router();
8
+
9
+ router.get('/:collection', async (req, res) => {
10
+ const Service = req.context.services.ItemsService;
11
+ const result = await readManyWithRelations({
12
+ collection: req.params.collection,
13
+ query: req.query,
14
+ options: serviceOptionsFromRequest(req),
15
+ ItemsServiceClass: Service,
16
+ });
17
+ res.json(result);
18
+ });
19
+
20
+ router.get('/:collection/:id', async (req, res) => {
21
+ const Service = req.context.services.ItemsService;
22
+ const data = await readOneWithRelations({
23
+ collection: req.params.collection,
24
+ id: req.params.id,
25
+ query: req.query,
26
+ options: serviceOptionsFromRequest(req),
27
+ ItemsServiceClass: Service,
28
+ });
29
+
30
+ if (!data) {
31
+ const error = new Error('Item not found');
32
+ error.code = 'NOT_FOUND';
33
+ throw error;
34
+ }
35
+
36
+ res.json({ data });
37
+ });
38
+
39
+ router.post('/:collection', async (req, res) => {
40
+ const Service = req.context.services.ItemsService;
41
+ const service = new Service(req.params.collection, serviceOptionsFromRequest(req));
42
+ const data = await service.createOne(req.body);
43
+ res.status(201).json({ data });
44
+ });
45
+
46
+ router.patch('/:collection/:id', async (req, res) => {
47
+ const Service = req.context.services.ItemsService;
48
+ const service = new Service(req.params.collection, serviceOptionsFromRequest(req));
49
+ const data = await service.updateOne(req.params.id, req.body);
50
+
51
+ if (!data) {
52
+ const error = new Error('Item not found or not accessible');
53
+ error.code = 'NOT_FOUND';
54
+ throw error;
55
+ }
56
+
57
+ res.json({ data });
58
+ });
59
+
60
+ router.delete('/:collection/:id', async (req, res) => {
61
+ const Service = req.context.services.ItemsService;
62
+ const service = new Service(req.params.collection, serviceOptionsFromRequest(req));
63
+ const deleted = await service.deleteOne(req.params.id);
64
+
65
+ if (!deleted) {
66
+ const error = new Error('Item not found or not accessible');
67
+ error.code = 'NOT_FOUND';
68
+ throw error;
69
+ }
70
+
71
+ res.status(204).end();
72
+ });
73
+
74
+ return router;
75
+ }
@@ -0,0 +1,45 @@
1
+ import express from 'express';
2
+
3
+ import { serviceOptionsFromRequest } from '../service-options.js';
4
+
5
+ function permissionsService(req) {
6
+ const Service = req.context.services.PermissionsService;
7
+ return new Service(serviceOptionsFromRequest(req));
8
+ }
9
+
10
+ function notFound(id) {
11
+ const error = new Error(`Permission not found: ${id}`);
12
+ error.code = 'PERMISSION_NOT_FOUND';
13
+ return error;
14
+ }
15
+
16
+ export function createPermissionsRouter() {
17
+ const router = express.Router();
18
+
19
+ router.get('/', async (req, res) => {
20
+ res.json({ data: await permissionsService(req).readMany() });
21
+ });
22
+
23
+ router.post('/', async (req, res) => {
24
+ const data = await permissionsService(req).createOne(req.body ?? {});
25
+ res.status(201).json({ data });
26
+ });
27
+
28
+ router.get('/:id', async (req, res) => {
29
+ const data = await permissionsService(req).readOne(req.params.id);
30
+ if (!data) throw notFound(req.params.id);
31
+ res.json({ data });
32
+ });
33
+
34
+ router.patch('/:id', async (req, res) => {
35
+ const data = await permissionsService(req).updateOne(req.params.id, req.body ?? {});
36
+ res.json({ data });
37
+ });
38
+
39
+ router.delete('/:id', async (req, res) => {
40
+ await permissionsService(req).deleteOne(req.params.id);
41
+ res.status(204).end();
42
+ });
43
+
44
+ return router;
45
+ }
@@ -0,0 +1,45 @@
1
+ import express from 'express';
2
+
3
+ import { serviceOptionsFromRequest } from '../service-options.js';
4
+
5
+ function rolesService(req) {
6
+ const Service = req.context.services.RolesService;
7
+ return new Service(serviceOptionsFromRequest(req));
8
+ }
9
+
10
+ function notFound(id) {
11
+ const error = new Error(`Role not found: ${id}`);
12
+ error.code = 'ROLE_NOT_FOUND';
13
+ return error;
14
+ }
15
+
16
+ export function createRolesRouter() {
17
+ const router = express.Router();
18
+
19
+ router.get('/', async (req, res) => {
20
+ res.json({ data: await rolesService(req).readMany() });
21
+ });
22
+
23
+ router.post('/', async (req, res) => {
24
+ const data = await rolesService(req).createOne(req.body ?? {});
25
+ res.status(201).json({ data });
26
+ });
27
+
28
+ router.get('/:id', async (req, res) => {
29
+ const data = await rolesService(req).readOne(req.params.id);
30
+ if (!data) throw notFound(req.params.id);
31
+ res.json({ data });
32
+ });
33
+
34
+ router.patch('/:id', async (req, res) => {
35
+ const data = await rolesService(req).updateOne(req.params.id, req.body ?? {});
36
+ res.json({ data });
37
+ });
38
+
39
+ router.delete('/:id', async (req, res) => {
40
+ await rolesService(req).deleteOne(req.params.id);
41
+ res.status(204).end();
42
+ });
43
+
44
+ return router;
45
+ }