Skip to content

Commit 999e8d0

Browse files
authored
Merge pull request #61 from sacconazzo/develop
granular publication based on access policy
2 parents d69c4b2 + 8dd543a commit 999e8d0

File tree

6 files changed

+61
-39
lines changed

6 files changed

+61
-39
lines changed

README.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,47 @@
22

33
> Compatible with latest Directus versions and packaged extensions.
44
5-
Directus Extension to include:
5+
Directus Extension providing:
66

7-
- a Swagger interface
8-
- configurable autogenerated openapi specifications file
9-
-- including custom endpoints definitions
10-
- validation middleware on your custom endpoints (based on your openapi specifications). See below for details
7+
- a Swagger UI interface (OpenAPI 3.x)
8+
- an autogenerated OpenAPI specification file (merged core + your custom endpoints)
9+
-- including custom endpoint definitions
10+
- optional validation middleware for your custom endpoints (based on merged OpenAPI spec). See details below
1111

1212
![workspace](assets/swagger.png)
1313

1414
## Prerequisites
1515

16-
Working in a Directus nodejs project
16+
You must already have a Directus Node.js project running.
1717

1818
Ref: https://github.com/directus/directus
1919

2020
## Installation
2121

22-
npm install directus-extension-api-docs
22+
npm install directus-extension-api-docs
2323

2424
- Swagger interface: by default `http://localhost:8055/api-docs`
2525
- Openapi documentation: by default `http://localhost:8055/api-docs/oas`
2626

2727
## Configuration (optional)
2828

29-
To include you custom endpoints in your documentation.
30-
31-
Create a `oasconfig.yaml` file under `/extensions` folder.
29+
To include your custom endpoints in the documentation, create an `oasconfig.yaml` file directly under the `/extensions` folder (recommended structure). Avoid placing it under `/extensions/endpoints` unless using Legacy mode.
3230

3331
Options:
3432

35-
- `docsPath` _optional_ path where the interface will be (default 'api-docs')
33+
- `docsPath` _optional_ interface base path (default 'api-docs'). Resulting URLs: `/<docsPath>` and `/<docsPath>/oas`.
3634
- `info` _optional_ openapi server info (default extract from package.json)
3735
- `tags` _optional_ openapi custom tags (will be merged with all standard and all customs tags)
38-
- `publishedTags` _optional_ if specified, will be published definitions only for specified tags
39-
- `paths` _optional_ openapi custom paths (will be merged with all standard and all customs paths)
40-
- `components` _optional_ openapi custom components (will be merged with all standard and all customs tags)
36+
- `publishedTags` _optional_ if specified, only operations containing at least one of these tags are kept; all other paths and unused tags are removed.
37+
- `paths` _optional_ custom path objects keyed by full path (e.g. `/my-custom-path/my-endpoint`). These are merged into Directus core paths.
38+
- `components` _optional_ custom components (schemas, securitySchemes, etc.) shallow-merged over core components.
39+
- `useAuthentication` _optional_ (default false). When true, `/api-docs` and `/api-docs/oas` stay publicly reachable: without valid auth they list only anonymous/public paths (no custom endpoints); with auth they list only paths permitted to that user under Directus Access Policies and custom endpoints.
4140

4241
Example below:
4342

4443
```
4544
docsPath: 'api-docs'
45+
useAuthentication: true
4646
info:
4747
title: my-directus-bo
4848
version: 1.5.0
@@ -69,15 +69,15 @@ components:
6969

7070
## Definitions (optional)
7171

72-
For each endpoint extension, you can define api's including a file `oas.yaml` in root path of your extension endpoint folder.
72+
For each endpoint extension, you can define OpenAPI partials by adding an `oas.yaml` file in the root of that endpoint's folder.
7373

7474
Properties:
7575

7676
- `tags` _optional_ openapi custom tags
7777
- `paths` _optional_ openapi custom paths
7878
- `components` _optional_ openapi custom components
7979

80-
Exemple below (`./extensions/my-endpoint-extensions/oas.yaml`) :
80+
Example below (`./extensions/my-endpoint-extensions/oas.yaml`):
8181

8282
```
8383
tags:
@@ -127,7 +127,7 @@ components:
127127

128128
### Legacy mode
129129

130-
Configuration and definitions can also be managed in this structure:
130+
Configuration and definitions can also be managed in this legacy structure (still supported, but prefer the simplified root placement):
131131

132132
```
133133
- ./extensions/
@@ -141,11 +141,11 @@ Configuration and definitions can also be managed in this structure:
141141

142142
## Validations (optional)
143143

144-
You can enable a request validations middleware based on your custom definitions.
144+
You can enable a request validation middleware based on your merged custom definitions.
145145

146-
Call `validate` function inside your custom endpoint source (`./extensions/my-endpoint-extensions/src/index.js`).
146+
Call the `validate` function inside your custom endpoint source (`./extensions/my-endpoint-extensions/src/index.js`).
147147

148-
Pass your `router`, `services`, `schema` and a list (_optional_) of endpoints you want to validate.
148+
Arguments: `validate(router, services, schema, paths?)`. `paths` (optional array) lets you restrict validation to only specific path keys from `oasconfig.yaml` instead of all custom paths.
149149

150150
Example below:
151151

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directus-extension-api-docs",
3-
"version": "2.1.16",
3+
"version": "2.2.0",
44
"description": "directus extension for swagger interface and openapi including custom endpoints definitions // custom endpoint validations middleware based on openapi",
55
"licence": "MIT",
66
"icon": "api",

src/index.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defineEndpoint } from '@directus/extensions-sdk';
33
// import { SchemaOverview } from '@directus/shared/types';
44
import { SchemaOverview } from '@directus/types';
55
import { Router, Request, Response, NextFunction } from 'express';
6-
import { getConfig, getOas, getPackage, merge, filterPaths } from './utils';
6+
import { getConfig, getOas, getOasAll, getPackage, merge, filterPaths } from './utils';
77

88
const swaggerUi = require('swagger-ui-express');
99
const OpenApiValidator = require('express-openapi-validator');
@@ -14,7 +14,7 @@ const id = config.docsPath;
1414

1515
async function validate(router: Router, services: any, schema: SchemaOverview, paths: Array<string>): Promise<Router> {
1616
if (config?.paths) {
17-
const oas = await getOas(services, schema);
17+
const oas = await getOasAll(services, schema);
1818

1919
// replace with custom endpoints
2020
if (paths) {
@@ -65,10 +65,13 @@ export default {
6565
router.use('/', swaggerUi.serve);
6666
router.get('/', swaggerUi.setup({}, options));
6767

68-
router.get('/oas', async (_req: Request, res: Response, next: NextFunction) => {
68+
router.get('/oas', async (req: Request, res: Response, next: NextFunction) => {
6969
try {
7070
const schema = await getSchema();
71-
const swagger = await getOas(services, schema);
71+
72+
const accountability = config.useAuthentication ? (req as any).accountability : { admin: true };
73+
74+
const swagger = await getOas(services, schema, accountability);
7275

7376
const pkg = await getPackage();
7477

@@ -77,22 +80,24 @@ export default {
7780
swagger.info.description = config.info.description || pkg?.description || swagger.info.description;
7881

7982
// inject custom-endpoints
80-
try {
81-
for (const path in config.paths) {
82-
swagger.paths[path] = config.paths[path];
83+
if (accountability.admin || accountability.user) {
84+
try {
85+
for (const path in config.paths) {
86+
swagger.paths[path] = config.paths[path];
87+
}
88+
89+
for (const tag of config.tags) {
90+
swagger.tags.push(tag);
91+
}
92+
93+
swagger.components = merge(config.components, swagger.components);
94+
} catch (e) {
95+
logger.info('No custom definitions');
8396
}
8497

85-
for (const tag of config.tags) {
86-
swagger.tags.push(tag);
87-
}
88-
89-
swagger.components = merge(config.components, swagger.components);
90-
} catch (e) {
91-
logger.info('No custom definitions');
98+
if (config.publishedTags?.length) filterPaths(config, swagger);
9299
}
93100

94-
if (config.publishedTags?.length) filterPaths(config, swagger);
95-
96101
res.json(swagger);
97102
} catch (error: any) {
98103
return next(new Error(error.message || error[0].message));

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface oasConfig {
33
info: any;
44
tags: Array<any>;
55
publishedTags: Array<string>;
6+
useAuthentication: boolean;
67
paths: {
78
[key: string]: any;
89
};

src/utils.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function getConfigRoot(): oasConfig {
2626
info: {},
2727
tags: [],
2828
publishedTags: [],
29+
useAuthentication: false,
2930
paths: {},
3031
components: {},
3132
};
@@ -43,6 +44,8 @@ function getConfigRoot(): oasConfig {
4344
config.docsPath = config.docsPath || defConfig.docsPath;
4445
config.info = config.info || defConfig.info;
4546
config.tags = config.tags || defConfig.tags;
47+
config.publishedTags = config.publishedTags || defConfig.publishedTags;
48+
config.useAuthentication = typeof config.useAuthentication === 'boolean' ? config.useAuthentication : defConfig.useAuthentication;
4649
config.paths = config.paths || defConfig.paths;
4750
config.components = config.components || defConfig.components;
4851
return config;
@@ -110,12 +113,12 @@ export function getConfig(): oasConfig {
110113
}
111114
}
112115

113-
export async function getOas(services: any, schema: SchemaOverview): Promise<oas> {
116+
export async function getOasAll(services: any, schema: SchemaOverview): Promise<oas> {
114117
if (oasBuffer) return JSON.parse(oasBuffer);
115118

116119
const { SpecificationService } = services;
117120
const service = new SpecificationService({
118-
accountability: { admin: true }, // null or accountability.admin = true needed
121+
accountability: { admin: true },
119122
schema,
120123
});
121124

@@ -124,6 +127,18 @@ export async function getOas(services: any, schema: SchemaOverview): Promise<oas
124127
return JSON.parse(oasBuffer);
125128
}
126129

130+
export async function getOas(services: any, schema: SchemaOverview, accountability: any): Promise<oas> {
131+
const { SpecificationService } = services;
132+
const service = new SpecificationService({
133+
accountability,
134+
schema,
135+
});
136+
137+
const oas = JSON.stringify(await service.oas.generate());
138+
139+
return JSON.parse(oas);
140+
}
141+
127142
export async function getPackage() {
128143
try {
129144
const workspaceDir = await findWorkspaceDir('.');

tests/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe('filterPaths', () => {
7878
const oasConfig: oasConfig = {
7979
docsPath: 'api-docs',
8080
info: {},
81+
useAuthentication: true,
8182
tags: [],
8283
components: {},
8384
publishedTags: ['tag2'],

0 commit comments

Comments
 (0)