Skip to content

Commit 7344d53

Browse files
committed
docs(*): flesh out generic example
1 parent a8d3816 commit 7344d53

File tree

9 files changed

+314
-2
lines changed

9 files changed

+314
-2
lines changed
File renamed without changes.
File renamed without changes.

examples/generic/.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public/
2+
.env
3+
4+
# Note that we want this example to always use the latest version of the main package so we don't lock
5+
yarn.lock
6+
7+
.now

examples/generic/README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generic Example
2+
3+
## Prerequisites
4+
5+
- Node 11.14+ (`fs.promises`)
6+
7+
## Instructions
8+
9+
1. Inside the root of the project (not this example directory), install its dependencies and link the library sources
10+
globally:
11+
```shell script
12+
yarn
13+
yarn link
14+
```
15+
1. Now move into this example directory, install its deps, and complete the link so the node server uses the sources
16+
locally:
17+
```shell script
18+
cd examples/generic/
19+
yarn
20+
yarn link netlify-cms-oauth-provider-node
21+
```
22+
2. Create a `.env` file in this directory with the following contents, filling in `OAUTH_CLIENT_ID` and
23+
`OAUTH_CLIENT_SECRET` with your GitHub OAuth app's ID and secret.
24+
```text
25+
DEBUG=netlify-cms-oauth-provider-node*
26+
NODE_ENV=development
27+
HOSTNAME=localhost
28+
PORT=3000
29+
OAUTH_CLIENT_ID=
30+
OAUTH_CLIENT_SECRET=
31+
```
32+
3. If you've forked this repository, update [`config.yml`](./config.yml) with your repo. Otherwise you will be in a
33+
read-only mode OR the login will fail since you (probably) won't have write access to this package's repository.
34+
4. Run the dev server:
35+
```shell script
36+
yarn start
37+
```
38+
5. Visit the local dev server at `http://localhost:3000`

examples/generic/admin.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Netlify CMS + netlify-cms-oauth-provider-node Now Test</title>
7+
</head>
8+
<body>
9+
<!-- Include the script that builds the page and powers Netlify CMS -->
10+
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
11+
</body>
12+
</html>

examples/generic/config.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
backend:
2+
name: github
3+
repo: bericp1/netlify-cms-oauth-provider-node
4+
branch: master
5+
base_url: http://localhost:3000
6+
auth_endpoint: api/admin/auth/begin
7+
publish_mode: editorial_workflow
8+
media_folder: "examples/common/images/uploads"
9+
collections:
10+
- name: "pages"
11+
label: "Pages"
12+
folder: "examples/common/pages"
13+
create: true
14+
slug: "{{slug}}"
15+
fields:
16+
- {label: "Title", name: "title", widget: "string"}
17+
- {label: "Body", name: "body", widget: "markdown"}

examples/generic/index.js

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
const http = require('http');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const frontmatter = require('front-matter');
5+
const marked = require('marked');
6+
const netlifyCmsOAuth = require('netlify-cms-oauth-provider-node');
7+
8+
const port = process.env.PORT || 3000;
9+
const hostname = process.env.HOSTNAME || 'localhost';
10+
11+
// Create the handlers, using env variables for the ones not explicitly specified.
12+
const netlifyCmsOAuthHandlers = netlifyCmsOAuth.createHandlers({
13+
origin: `${hostname}:${port}`,
14+
completeUrl: `http://${hostname}${port === 80 ? '' : `:${port}`}/api/admin/auth/complete`,
15+
adminPanelUrl: `http://${hostname}${port === 80 ? '' : `:${port}`}/admin`,
16+
oauthProvider: 'github',
17+
}, {
18+
useEnv: true,
19+
});
20+
21+
/**
22+
* Return a 404 to the user. This is our fallback route.
23+
*
24+
* @param {IncomingMessage} req
25+
* @param {OutgoingMessage} res
26+
* @return {Promise<void>}
27+
*/
28+
async function handleNotFound(req, res) {
29+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
30+
res.end('Not found.');
31+
}
32+
33+
/**
34+
* Creates a request handler that performs a redirect.
35+
*
36+
* TODO This probably doesn't need to be a factory function / decorator like it is based on the usages below.
37+
*
38+
* @param {string} to
39+
* @param {number=302} status
40+
* @return {function(req: IncomingMessage, res: OutgoingMessage): void}
41+
*/
42+
function createRedirectHandler(to, status = 302) {
43+
return function (req, res) {
44+
res.writeHead(status, { Location: to, 'Content-Type': 'text/html; charset=utf-8' });
45+
res.end(`Redirecting to <a href="${to.replace('"', '&quot;')}">${to}</a>...`);
46+
};
47+
}
48+
49+
/**
50+
* Creates a request handler that serves a specific static file.
51+
*
52+
* @param {string} filename An absolute file path or one relative to the server root directory
53+
* @param {(string|null)=} type A valid mime type. If not set, no Content-Type will be sent to the client.
54+
* @return {function(req: IncomingMessage, res: OutgoingMessage, ctx: object): Promise<void>}
55+
*/
56+
function createStaticFileHandler(filename, type = null) {
57+
return async function (req, res, ctx) {
58+
try {
59+
// Get the full absolute path to the file.
60+
const fullPath = path.resolve(__dirname, filename);
61+
// Get its contents
62+
// TODO Improve by using streams or just node-static
63+
const fileContents = await fs.promises.readFile(fullPath, { encoding: 'utf8' });
64+
// Generate the headers containing the mime type
65+
const headers = type ? { 'Content-Type': type } : {};
66+
// Write the header and file contents out
67+
res.writeHead(200, headers);
68+
res.write(fileContents);
69+
} catch (error) {
70+
// If an error occurred, we'll just write out a 404
71+
// TODO Improve error handling, i.e. a 500 when this is an unexpected error
72+
return handleNotFound(req, res, ctx);
73+
} finally {
74+
// Always end the response
75+
res.end();
76+
}
77+
}
78+
}
79+
80+
/**
81+
* Handles the request to kick off the admin OAuth flow using this library.
82+
*
83+
* @param {IncomingMessage} req
84+
* @param {OutgoingMessage} res
85+
* @return {Promise<void>}
86+
*/
87+
async function handleAdminAuthBegin(req, res) {
88+
// Generate the auth URI and redirect the user there.
89+
const authorizationUri = await netlifyCmsOAuthHandlers.begin();
90+
return createRedirectHandler(authorizationUri)(req, res);
91+
}
92+
93+
/**
94+
* Handles the request to complete the admin OAuth flow using this library.
95+
*
96+
* @param {IncomingMessage} req
97+
* @param {OutgoingMessage} res
98+
* @param {URL} parsedRequest
99+
* @return {Promise<void>}
100+
*/
101+
async function handleAdminAuthComplete(req, res, { parsedRequest }) {
102+
// Extract the code from the query parameters
103+
const code = parsedRequest.searchParams.get('code') || null;
104+
// Allow the library to complete the oauth flow, exchange the auth code for an access token, and generate the popup HTML that
105+
// will hand it off to the netlify-cms admin panel using the `postMessage` API.
106+
const content = await netlifyCmsOAuthHandlers.complete(code);
107+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
108+
res.end(content);
109+
}
110+
111+
/**
112+
* Will serve the netlify-cms config file.
113+
*
114+
* @type {function(IncomingMessage, OutgoingMessage): Promise<void>}
115+
*/
116+
const handleAdminConfig = createStaticFileHandler('config.yml', 'text/yaml; charset=utf-8');
117+
118+
/**
119+
* Will serve the netlify-cms main HTML file.
120+
*
121+
* @type {function(IncomingMessage, OutgoingMessage): Promise<void>}
122+
*/
123+
const handleAdmin = createStaticFileHandler('admin.html', 'text/html; charset=utf-8');
124+
125+
/**
126+
* Possibly handle a requested markdown page. The provided route is compared to our available markdown page files
127+
* and if one is found, it's compiled via `front-matter` and `marked` served to the user as HTML. Otherwise it does
128+
* nothing.
129+
*
130+
* @param {IncomingMessage} req
131+
* @param {OutgoingMessage} res
132+
* @param {string} route
133+
* @return {Promise<boolean>} Resolves with true if the request was handled (a page was found and rendered) or false
134+
* otherwise.
135+
*/
136+
async function handlePage(req, res, { route }) {
137+
// Resolve the path to the possible markdown file
138+
const fullPathToPossibleMarkdownFile = path.resolve(
139+
__dirname,
140+
'../common/pages',
141+
`${route.replace(/^\//, '')}.md`,
142+
);
143+
// Attempt to read the file, returning false ("no I did not handle this request") if any error occurs reading the file.
144+
let markdownFileContents = null;
145+
try {
146+
markdownFileContents = await fs.promises.readFile(fullPathToPossibleMarkdownFile, { encoding: 'utf8' });
147+
} catch (ignored) {
148+
return false;
149+
}
150+
// Parse the file first using `front-matter` and `marked`
151+
const fileFrontmatter = frontmatter(markdownFileContents);
152+
const htmlFileContents = marked(fileFrontmatter.body);
153+
// Generate the HTML, using the frontmatter to generate the title.
154+
const finalHtml = `<html lang="en"><head><title>${fileFrontmatter.attributes.title || ''}</title>`
155+
+ `</head><body>\n${htmlFileContents}</body></html>`;
156+
// Serve the HTML to the user.
157+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
158+
res.end(finalHtml);
159+
return true;
160+
}
161+
162+
/**
163+
* Our server request handler. Node's `http` server module will run this function for every incoming request.
164+
*
165+
* @param {IncomingMessage} req
166+
* @param {OutgoingMessage} res
167+
* @return {Promise<void>}
168+
*/
169+
async function handleRequest(req, res) {
170+
// Parse the request
171+
const parsedRequest = new URL(req.url, `http://${req.headers.host}`);
172+
const route = parsedRequest.pathname.toLowerCase().trim().replace(/\/+$/, '') || '/';
173+
174+
// Generate a context object that gets passed to all of our handlers so they have extra info about the request
175+
const ctx = { parsedRequest, route };
176+
177+
// Redirect to canonical routes if the original route doesn't match the final processed route.
178+
if (ctx.route !== ctx.parsedRequest.pathname) {
179+
console.log(`Redirecting: '${ctx.parsedRequest.pathname}' -> '${ctx.route}'`);
180+
await createRedirectHandler(`http://${req.headers.host}${ctx.route}`, 301)(req, res, ctx);
181+
return;
182+
}
183+
184+
// Manually suppoort some aliases
185+
// TODO Also redirect from one to the other so that one is treated as canonical
186+
// TODO Abstract this out into a Map or something
187+
if (ctx.route === '/') {
188+
console.log(`Serving alias: '${ctx.route}' => '/home'...`);
189+
ctx.route = '/home';
190+
} else {
191+
console.log(`Serving: '${ctx.route}'`);
192+
}
193+
194+
// Simplistic routing using a good, old-fashioned set of conditionals
195+
if (ctx.route === '/api/admin/auth/begin') {
196+
return handleAdminAuthBegin(req, res, ctx);
197+
} else if (ctx.route === '/api/admin/auth/complete') {
198+
return handleAdminAuthComplete(req, res, ctx);
199+
} else if (ctx.route === '/admin/config.yml' || ctx.route === '/config.yml') {
200+
return handleAdminConfig(req, res, ctx);
201+
} else if (ctx.route.startsWith('/admin')) {
202+
return handleAdmin(req, res, ctx);
203+
}
204+
205+
// If none of the above explicit routes matched, see if we can match against the markdown pages
206+
const handledPage = await handlePage(req, res, ctx);
207+
208+
// If the markdown pages didn't match, finally just send a 404
209+
if (!handledPage) {
210+
return handleNotFound(req, res, ctx);
211+
}
212+
}
213+
214+
// Create the server
215+
// TODO Support `https`
216+
const server = http.createServer(handleRequest);
217+
218+
// Listen on the desired port
219+
server.listen(port, () => {
220+
console.log(`Listening on port ${port}...`);
221+
});

examples/generic/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "netlify-cms-oauth-provider-node-now-example",
3+
"private": true,
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node -r dotenv/config index.js"
7+
},
8+
"engines": {
9+
"node": ">=11.14.0"
10+
},
11+
"dependencies": {
12+
"dotenv": "^8.2.0",
13+
"front-matter": "^3.1.0",
14+
"marked": "^0.8.2",
15+
"netlify-cms-oauth-provider-node": "latest"
16+
}
17+
}

examples/now/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ backend:
55
base_url: http://localhost:3000
66
auth_endpoint: api/begin
77
publish_mode: editorial_workflow
8-
media_folder: "examples/now/images/uploads"
8+
media_folder: "examples/common/images/uploads"
99
collections:
1010
- name: "pages"
1111
label: "Pages"
12-
folder: "examples/now/pages"
12+
folder: "examples/common/pages"
1313
create: true
1414
slug: "{{slug}}"
1515
fields:

0 commit comments

Comments
 (0)