Skip to content

Instantly share code, notes, and snippets.

@igk1972
Last active January 20, 2018 13:54
Show Gist options
  • Save igk1972/367b68b26495f47c1b571cdd38f720c5 to your computer and use it in GitHub Desktop.
Save igk1972/367b68b26495f47c1b571cdd38f720c5 to your computer and use it in GitHub Desktop.
Netlify CMS - api for file-system backend support JWT
.git
.gitignore
.dockerignore
docker-*.yml
node_modules
Makefile
FROM alpine:3.6
RUN apk add --update-cache --no-cache nodejs yarn
ENV NODE_ENV=production
EXPOSE 8080
ENTRYPOINT ["node", "main.js"]
CMD []
RUN mkdir -p /app
WORKDIR /app/
COPY . /app/
RUN cd /app && yarn --prod --cache-folder /tmp/yarn && rm -fr /tmp/yarn
/**
*
* Based on code https://github.com/netlify/netlify-cms/blob/api-file-system/scripts/fs/fs-api.js (Tony Alves)
*
**/
const fs = require('fs');
const path = require('path');
let rootSite = process.env.SITE_ROOT || path.join(__dirname,'..','site');
const siteRoot = {
setPath: (value) => {
rootSite = value;
},
dir: rootSite
};
console.log(`Site path is ${ siteRoot.dir }`);
module.exports = {
files: (dirname) => {
const name = "Files";
const read = (cb) => {
if (!cb) throw new Error("Invalid call to files.read - requires a callback function(content)");
const thispath = path.join(siteRoot.dir, dirname);
const files = fs.existsSync(thispath) ? fs.readdirSync(thispath) : [];
const filelist = [];
files.forEach(function(element) {
const filePath = path.join(thispath, element);
const stats = fs.statSync(filePath);
if (stats.isFile()) {
filelist.push({ name: element, path: `${ dirname }/${ element }`, stats, type: "file" });
}
}, this);
cb(filelist);
};
return { read, name };
},
file: (id) => {
const name = "File";
const thisfile = path.join(siteRoot.dir, id);
let stats;
try {
stats = fs.statSync(thisfile);
} catch (err) {
stats = { };
}
/* GET-Read an existing file */
const read = (cb) => {
if (!cb) throw new Error("Invalid call to file.read - requires a callback function(content)");
if (stats.isFile && stats.isFile()) {
fs.readFile(thisfile, 'utf8', (err, data) => {
if (err) {
cb({ error: err });
} else {
cb(data);
}
});
} else {
throw new Error("Invalid call to file.read - object path is not a file!");
}
};
/* POST-Create a NEW file, ERROR if exists */
const create = (body, cb) => {
fs.writeFile(thisfile, body.content, { encoding: body.encoding, flag: 'wx' }, (err) => {
if (err) {
cb({ error: err });
} else {
cb(body.content);
}
});
};
/* PUT-Update an existing file */
const update = (body, cb) => {
fs.writeFile(thisfile, body.content, { encoding: body.encoding, flag: 'w' }, (err) => {
if (err) {
cb({ error: err });
} else {
cb(body.content);
}
});
};
/* DELETE an existing file */
const del = (cb) => {
fs.unlink(thisfile, (err) => {
if (err) {
cb({ error: err });
} else {
cb(`Deleted File ${ thisfile }`);
}
});
};
return { read, create, update, del, stats };
},
};
/**
*
* Based on code https://github.com/netlify/netlify-cms/blob/api-file-system/scripts/fs/fs-express-api.js (Tony Alves)
*
**/
const bodyParser = require('body-parser');
const multer = require('multer');
const fsAPI = require('./fs-api');
const JWT = require('jwt-decode');
/* Express allows for app object setup to handle paths (our api routes) */
module.exports = function(app) {
const upload = multer(); // for parsing multipart/form-data
const uploadLimit = '50mb'; // express has a default of ~20Kb
app.use(bodyParser.json({ limit: uploadLimit })); // for parsing application/json
app.use(bodyParser.urlencoded({ limit: uploadLimit, extended: true, parameterLimit:50000 })); // for parsing application/x-www-form-urlencoded
// We will look at every route to bypass any /api route from the react app
app.use('/:path', function(req, res, next) {
const response = { };
// if the path is api, skip to the next route
if (req.params.path === 'api') {
if (process.env.NODE_ENV == 'production') {
if (req.headers['authorization'] && req.headers['authorization'].startsWith('Bearer')){
const jwt_token = req.headers['authorization'].substr(7);
const jwt_data = JWT(jwt_token);
next('route');
} else {
response.status = 403;
response.error = 'Need authorization';
res.status(response.status).json(response);
}
} else {
next('route');
}
}
// otherwise pass the control out of this middleware to the next middleware function in this stack (back to regular)
else next();
});
app.use('/api', function(req, res, next) {
const response = { route: '/api', url: req.originalUrl };
if (req.originalUrl === "/api" || req.originalUrl === "/api/") {
// if the requested url is the root, , respond Error!
response.status = 500;
response.error = 'This is the root of the API';
res.status(response.status).json(response);
} else {
// continue to the next sub-route ('/api/:path')
next('route');
}
});
/* Define custom handlers for api paths: */
app.use('/api/:path', function(req, res, next) {
const response = { route: '/api/:path', path: req.params.path, params: req.params };
if (req.params.path && req.params.path in fsAPI) {
// all good, route exists in the api
next('route');
} else {
// sub-route was not found in the api, respond Error!
response.status = 500;
response.error = `Invalid path ${ req.params.path }`;
res.status(response.status).json(response);
}
});
/* Files */
/* Return all the files in the starting path */
app.get('/api/files', function(req, res, next) {
const response = { route: '/api/files' };
try {
fsAPI.files('./').read((contents) => {
res.json(contents);
});
} catch (err) {
response.status = 500;
response.error = `Could not get files - code [${ err.code }]`;
response.internalError = err;
res.status(response.status).send(response);
}
});
/* Return all the files in the passed path */
app.get('/api/files/:path', function(req, res, next) {
const response = { route: '/api/files/:path', params: req.params, path: req.params.path };
try {
fsAPI.files(req.params.path).read((contents) => {
res.json(contents);
});
} catch (err) {
response.status = 500;
response.error = `Could not get files for ${ req.params.path } - code [${ err.code }]`;
response.internalError = err;
res.status(response.status).send(response);
}
});
/* Capture Unknown extras and handle path (ignore?) */
app.get('/api/files/:path/**', function(req, res, next) {
const response = { route: '/api/files/:path/**', params: req.params, path: req.params.path };
const filesPath = req.originalUrl.substring(11, req.originalUrl.split('?', 1)[0].length);
try {
fsAPI.files(filesPath).read((contents) => {
res.json(contents);
});
} catch (err) {
response.status = 500;
response.error = `Could not get files for ${ filesPath } - code [${ err.code }]`;
response.internalError = err;
res.status(response.status).send(response);
}
});
/* File */
app.get('/api/file', function(req, res, next) {
const response = { error: 'Id cannot be empty for file', status: 500, path: res.path };
res.status(response.status).send(response);
});
app.get('/api/file/:id', function(req, res, next) {
const response = { route: '/api/file/:id', id: req.params.id };
const allDone = (contents) => {
if (contents.error) {
response.status = 500;
response.error = `Could not read file ${ req.params.id } - code [${ contents.error.code }]`;
response.internalError = contents.error;
res.status(response.status).send(response);
} else {
res.json(contents);
}
};
if (req.params.id) {
fsAPI.file(req.params.id).read(allDone);
} else {
response.status = 500;
response.error = `Invalid id for File ${ req.params.id }`;
res.status(response.status).send(response);
}
});
/* Capture Unknown extras and ignore the rest */
app.get('/api/file/:id/**', function(req, res, next) {
const response = { route: '/api/file/:id', id: req.params.id, method:req.method };
const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length);
const allDone = (contents) => {
if (contents.error) {
response.status = 500;
response.error = `Could not read file ${ filePath } - code [${ contents.error.code }]`;
response.internalError = contents.error;
res.status(response.status).send(response);
} else {
res.json(contents);
}
};
if (filePath) {
fsAPI.file(filePath).read(allDone);
} else {
response.status = 500;
response.error = `Invalid path for File ${ filePath }`;
res.status(response.status).send(response);
}
});
/* Create file if path does not exist */
app.post('/api/file/:id/**', upload.array(), function(req, res, next) {
const response = { route: '/api/file/:id', id: req.params.id, method:req.method };
const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length);
const allDone = (contents) => {
if (contents.error) {
response.status = 500;
response.error = `Could not create file ${ filePath } - code [${ contents.error.code }]`;
response.internalError = contents.error;
res.status(response.status).send(response);
} else {
res.json(contents);
}
};
if (filePath) {
fsAPI.file(filePath).create(req.body, allDone);
} else {
response.status = 500;
response.error = `Invalid path for File ${ filePath }`;
res.status(response.status).send(response);
}
});
/* Update file, error on path exists */
app.put('/api/file/:id/**', upload.array(), function(req, res, next) {
const response = { route: '/api/file/:id', id: req.params.id, method:req.method };
const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length);
const allDone = (contents) => {
if (contents.error) {
response.status = 500;
response.error = `Could not update file ${ filePath } - code [${ contents.error.code }]`;
response.internalError = contents.error;
res.status(response.status).send(response);
} else {
res.json(contents);
}
};
if (filePath) {
fsAPI.file(filePath).update(req.body, allDone);
} else {
response.status = 500;
response.error = `Invalid path for File ${ filePath }`;
res.status(response.status).send(response);
}
});
/* Delete file, error if no file */
app.delete('/api/file/:id/**', function(req, res, next) {
const response = { route: '/api/file/:id', id: req.params.id, method:req.method };
const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length);
const allDone = (contents) => {
if (contents.error) {
response.status = 500;
response.error = `Could not delete file ${ filePath } - code [${ contents.error.code }]`;
response.internalError = contents.error;
res.status(response.status).send(response);
} else {
res.json(contents);
}
};
if (filePath) {
fsAPI.file(filePath).del(allDone);
} else {
response.status = 500;
response.error = `Invalid path for File ${ filePath }`;
res.status(response.status).send(response);
}
});
};
const express = require('express')
module.exports = function(app) {
app.use('/', express.static('site/static/admin'))
app.use('/media', express.static('site/static/media'))
};
const app = require('express')()
app.set('port', process.env.PORT || 8080)
require('./fs-express-static')(app)
require('./fs-express-api')(app)
app.disable('x-powered-by')
const server = app.listen(app.get('port'), () => console.log(`Server is running on port ${server.address().port}`))
process.on('SIGINT', function() {
if (process.env.NODE_ENV == 'development') {
console.log('Closed server');
process.exit(0);
}
console.log('Graceful shutdown server');
server.close(function () {
console.log('Closed server');
});
});
build:
docker -t netlify-cms-api_nodejs .
start:
docker run -d --name netlify-cms_admin -p 8080:8080 -v /path/to/project/site:/app/site -e NODE_ENV=development netlify-cms-api_nodejs
{
"name": "admin-netlify-cms",
"private": true,
"scripts": {
"start": "node main.js ${@}"
},
"devDependencies": {
"babel-core": "^6.0.15",
"babel-preset-env": "^1.6.0"
},
"dependencies": {
"express": "^4.16.2",
"jwt-decode": "^2.2.0",
"multer": "^1.3.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment