Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff10a23e78 | |||
| c2851ea537 | |||
| 98d767a201 | |||
| 955189d08a |
+7
-1
@@ -9,9 +9,15 @@
|
||||
.claude
|
||||
*.md
|
||||
# README.md and tos.md are both read at runtime (tos.md is loaded by
|
||||
# routes/index.js at boot), so they must stay in the build context.
|
||||
# routes/index.js at boot). DEPLOYMENT.md/API.md/directory_spec.md/docs/*.md
|
||||
# are read at runtime too, by routes/docs.js -- all must stay in the build
|
||||
# context.
|
||||
!README.md
|
||||
!tos.md
|
||||
!DEPLOYMENT.md
|
||||
!API.md
|
||||
!directory_spec.md
|
||||
!docs/**/*.md
|
||||
|
||||
# Tests
|
||||
nodejs/tests/
|
||||
|
||||
@@ -95,6 +95,14 @@ COPY nodejs/public ./public
|
||||
# level above the nodejs/ app dir). Without this the app crashes on startup.
|
||||
COPY tos.md /tos.md
|
||||
|
||||
# Documentation, served in-app at /docs (routes/docs.js) so it's readable
|
||||
# without internet access. Same flattened-path convention as tos.md above.
|
||||
COPY README.md /README.md
|
||||
COPY DEPLOYMENT.md /DEPLOYMENT.md
|
||||
COPY API.md /API.md
|
||||
COPY directory_spec.md /directory_spec.md
|
||||
COPY docs /docs
|
||||
|
||||
# Baked commit hash from the gitinfo stage (see build_info.js).
|
||||
COPY --from=gitinfo /commit.txt ./.build_commit
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ app.use('/static', express.static(path.join(__dirname, 'public'), {maxAge: '1h'}
|
||||
// Routes for front end content.
|
||||
app.use('/', require('./routes/index'));
|
||||
|
||||
// Local, in-app copy of the project's documentation (README, DEPLOYMENT,
|
||||
// API.md, docs/*) -- public, no auth, so it's readable even by a locked-out
|
||||
// admin or an air-gapped operator with no route to GitHub Pages.
|
||||
app.use('/docs', require('./routes/docs'));
|
||||
|
||||
// API routes for authentication.
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
|
||||
|
||||
@@ -40,3 +40,11 @@ exports.invite = rateLimit({
|
||||
limit: 20,
|
||||
handler: handler({ name: 'RateLimitError', message: 'Too many requests, try again later.' }),
|
||||
});
|
||||
|
||||
// Public, unauthenticated, reads from disk on every request -- generous
|
||||
// since it's just docs, but still throttled per IP.
|
||||
exports.docs = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
limit: 120,
|
||||
handler: handler({ name: 'RateLimitError', message: 'Too many requests, try again later.' }),
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"author": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const router = require('express').Router();
|
||||
const {marked} = require('marked');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const buildInfo = require('../utils/build_info');
|
||||
const rateLimit = require('../middleware/rate_limit');
|
||||
|
||||
const values = {
|
||||
title: conf.environment !== 'production' ? `dev` : '',
|
||||
titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '',
|
||||
name: conf.name,
|
||||
...buildInfo,
|
||||
};
|
||||
|
||||
// Full local copy of the project's documentation, rendered server-side --
|
||||
// so an operator running air-gapped (no route to GitHub Pages, where this
|
||||
// content otherwise only lives) can still read it from the running app.
|
||||
// An explicit slug -> file allowlist, never a user-suppliable path, so
|
||||
// there's no way to make this read outside the doc set below.
|
||||
// docs/deployment.md is deliberately excluded -- it's just a stub pointing
|
||||
// back at the root DEPLOYMENT.md (see docs/deployment.md itself), which is
|
||||
// already covered by the "deployment" entry.
|
||||
const DOCS = {
|
||||
overview: {title: 'Overview', file: path.join(__dirname, '../../README.md')},
|
||||
deployment: {title: 'Deployment', file: path.join(__dirname, '../../DEPLOYMENT.md')},
|
||||
api: {title: 'API Reference', file: path.join(__dirname, '../../API.md')},
|
||||
ldap: {title: 'LDAP', file: path.join(__dirname, '../../docs/ldap.md')},
|
||||
oauth: {title: 'OAuth', file: path.join(__dirname, '../../docs/oauth.md')},
|
||||
configuration: {title: 'Configuration', file: path.join(__dirname, '../../docs/configuration.md')},
|
||||
'directory-spec': {title: 'Directory Spec (draft)', file: path.join(__dirname, '../../directory_spec.md')},
|
||||
};
|
||||
|
||||
const docList = Object.entries(DOCS).map(([slug, d]) => ({slug, title: d.title}));
|
||||
|
||||
// README.md links its screenshots as repo-relative "docs/images/...", which
|
||||
// only resolves correctly on GitHub. Serve that same folder here and rewrite
|
||||
// the rendered markup to point at it absolutely, so the images work when
|
||||
// read from /docs/overview too.
|
||||
router.use('/images', require('express').static(path.join(__dirname, '../../docs/images')));
|
||||
function fixImagePaths(html) {
|
||||
return html.replace(/(["(])docs\/images\//g, '$1/docs/images/');
|
||||
}
|
||||
|
||||
router.use(rateLimit.docs);
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
res.render('docs_index', {...values, docs: docList});
|
||||
});
|
||||
|
||||
router.get('/:slug', function(req, res, next) {
|
||||
const doc = DOCS[req.params.slug];
|
||||
if (!doc) return next({status: 404, message: 'Doc not found'});
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(doc.file, 'utf8');
|
||||
res.render('docs_page', {
|
||||
...values,
|
||||
docs: docList,
|
||||
currentSlug: req.params.slug,
|
||||
docTitle: doc.title,
|
||||
docHtml: fixImagePaths(marked(content)),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -10,6 +10,9 @@
|
||||
<a href="https://github.com/theta42/sso-manager-node/blob/master/LICENSE" target="_blank" class="text-light">MIT License</a>
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-3">
|
||||
<a href="/docs" class="text-light text-decoration-none">
|
||||
<i class="fa-solid fa-book"></i> Docs
|
||||
</a>
|
||||
<a href="https://github.com/theta42/sso-manager-node" target="_blank" class="text-light text-decoration-none">
|
||||
<i class="fa-brands fa-github"></i> GitHub
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<%- include('top') %>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg mt-4 mb-4">
|
||||
<div class="card-header shadow">
|
||||
<i class="fa-solid fa-book"></i> Documentation
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
A local copy of this project's documentation, readable from the
|
||||
running app -- no internet access required.
|
||||
</p>
|
||||
<ul class="list-group">
|
||||
<% docs.forEach(function(doc){ %>
|
||||
<li class="list-group-item">
|
||||
<a href="/docs/<%= doc.slug %>"><%= doc.title %></a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('bottom') %>
|
||||
@@ -0,0 +1,29 @@
|
||||
<%- include('top') %>
|
||||
<div class="row">
|
||||
<div class="col-md-3 d-none d-md-block">
|
||||
<div class="card shadow-lg mt-4 mb-4">
|
||||
<div class="card-header shadow">
|
||||
<i class="fa-solid fa-book"></i> Documentation
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<% docs.forEach(function(doc){ %>
|
||||
<a href="/docs/<%= doc.slug %>"
|
||||
class="list-group-item list-group-item-action<%= doc.slug === currentSlug ? ' active' : '' %>">
|
||||
<%= doc.title %>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-lg mt-4 mb-4">
|
||||
<div class="card-header shadow">
|
||||
<i class="fa-solid fa-file-lines"></i> <%= docTitle %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%- docHtml %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('bottom') %>
|
||||
@@ -24,12 +24,6 @@
|
||||
<script type="text/javascript" src="/static-modules/moment/moment.js"></script>
|
||||
<script type="text/javascript" src="/static/lib/js/app-base.js"></script>
|
||||
<script type="text/javascript" src="/static/js/app.js"></script>
|
||||
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user