Compare commits

...

19 Commits

Author SHA1 Message Date
wmantly 88cf5cf281 Merge pull request #149 from theta42/bump-1.1.4
Bump version to 1.1.4
2026-07-16 17:45:22 -04:00
wmantly 526545ee68 Bump version to 1.1.4; update CHANGELOG 2026-07-16 17:44:00 -04:00
wmantly ecf064b9ae Merge pull request #148 from theta42/white-label
White-label: title/logo now driven by conf
2026-07-16 17:36:55 -04:00
wmantly fd71485960 White-label: title/logo now driven by conf (closes #45)
<title>, the navbar brand text, and the logo were all hardcoded
"Proxy - Theta 42"/"Dynamic Proxy". New conf.name/conf.logo keys
(defaults matching current text/asset) thread through the existing
values object pattern in routes/render.js and routes/docs.js, and
top.ejs now renders <%- name %>/<%- logo %> for the title and a new
navbar logo image.

Footer (copyright, theta42.com link, GitHub/license links) and the
existing favicon.svg are left as-is -- open-source attribution and a
distinct, already-working icon asset, not deployment branding.
2026-07-16 17:35:49 -04:00
wmantly 1c29ba7206 Merge pull request #147 from theta42/bump-1.1.3
Bump version to 1.1.3
2026-07-16 16:04:24 -04:00
wmantly 17e9ef2783 Bump version to 1.1.3; update CHANGELOG 2026-07-16 16:02:59 -04:00
wmantly 983f2a71f0 Merge pull request #146 from theta42/add-changelog
Add CHANGELOG.md, serve it in-app at /docs/changelog
2026-07-16 16:00:31 -04:00
wmantly edf60b3e3d Add CHANGELOG.md, serve it in-app at /docs/changelog (closes theta42/theta-env#43)
GitHub Releases already carried real changelog notes per tag, but
those require internet access to view -- exactly what the /docs
route exists to avoid. CHANGELOG.md is a committed, Keep-a-Changelog
style file (backfilled from the v1.1.0/v1.1.1/v1.1.2 release notes),
linked from README and served at /docs/changelog alongside the rest
of the project's docs.
2026-07-16 15:59:16 -04:00
wmantly 1f10d0db14 Revert "Add CHANGELOG.md, serve it in-app at /docs/changelog (closes theta42/theta-env#43)"
This reverts commit 4bf1768529.
2026-07-16 15:59:01 -04:00
wmantly 4bf1768529 Add CHANGELOG.md, serve it in-app at /docs/changelog (closes theta42/theta-env#43)
GitHub Releases already carried real changelog notes per tag, but
those require internet access to view -- exactly what the /docs
route exists to avoid. CHANGELOG.md is a committed, Keep-a-Changelog
style file (backfilled from the v1.1.0/v1.1.1/v1.1.2 release notes),
linked from README and served at /docs/changelog alongside the rest
of the project's docs.
2026-07-16 15:58:26 -04:00
wmantly ed275acbe7 Merge pull request #145 from theta42/bump-1.1.2
Bump version to 1.1.2
2026-07-16 15:38:15 -04:00
wmantly 8565f4aa27 Bump version to 1.1.2 2026-07-16 15:36:50 -04:00
wmantly 8c22d69e44 Merge pull request #144 from theta42/airgap-and-docs
Air-gap fixes + in-app /docs
2026-07-16 15:30:21 -04:00
wmantly c71b23ef82 docs.js: rate-limit the doc routes (CodeQL: missing rate limiting)
Public route reading from disk on every request with no throttling
-- add a per-IP limiter matching the routes/auth.js/routes/host.js
convention already used elsewhere in this repo.
2026-07-16 15:29:06 -04:00
wmantly 7d9c63b049 Air-gap fixes + in-app /docs (README/DEPLOYMENT/api.md/docs/*)
Air-gap:
- DynamicRecord.refreshAll() called getPublicIp() (api.ipify.org,
  icanhazip.com, ifconfig.me) every 4h on a timer regardless of
  whether any dynamic records were configured -- the one background
  call in the repo not actually gated by feature use. Now skips the
  lookup entirely when there's nothing to refresh.
- Removed the stray, unauthenticated GET /test page (a leftover
  jq-repeat demo) that loaded jQuery + Mustache from external CDNs.
- Removed a dead IE<9-only html5shim script tag pointing at a domain
  that no longer resolves.

Docs:
- New GET /docs (index) and /docs/:slug routes render this project's
  own README, DEPLOYMENT, api.md, and docs/*.md server-side via
  marked (new dependency) -- so the documentation is readable from
  the running app with no route to GitHub Pages, where it otherwise
  only lives. Public, no auth, same tier as the health endpoint.
- .dockerignore/Dockerfile updated: docs/, DEPLOYMENT.md, and
  nodejs/api.md were previously excluded from the image entirely
  ("served via GitHub Pages, not from the image") -- now copied in
  alongside README.md/tos.md-style, since they're needed at runtime.
2026-07-16 15:26:10 -04:00
wmantly 5a685e9dd7 Merge pull request #143 from theta42/bump-1.1.1
Bump version to 1.1.1
2026-07-16 13:58:10 -04:00
wmantly f98c4ec44f Bump version to 1.1.1 2026-07-16 13:56:57 -04:00
wmantly 8deecdba73 Merge pull request #142 from theta42/duckdns-validate-via-txt
DuckDNS: validate token via a TXT write, not the A/AAAA record
2026-07-16 12:56:24 -04:00
wmantly 3e90062255 DuckDNS: validate token via a TXT write, not the A/AAAA record
listDomains() used to validate the token by calling DuckDNS's update
endpoint with ip/ipv6 omitted, which makes DuckDNS auto-detect and
apply this host's public IP -- so adding a provider instantly
repointed the domain. Validate via a fixed TXT marker instead, which
DuckDNS's API supports independently and doesn't touch routing.
2026-07-16 12:55:06 -04:00
18 changed files with 254 additions and 98 deletions
+6 -2
View File
@@ -14,8 +14,6 @@ ops/cookbooks/
ops/roles/ ops/roles/
ops/proxy.service ops/proxy.service
# Docs site (served via GitHub Pages, not from the image).
docs/
.github/ .github/
# Git + editor + secrets. # Git + editor + secrets.
@@ -24,8 +22,14 @@ docs/
# nodejs/utils/build_info.js), then it's discarded before the final stage. # nodejs/utils/build_info.js), then it's discarded before the final stage.
# It never ends up in the final image. # It never ends up in the final image.
.gitignore .gitignore
# docs/ and the doc files below ARE needed in the image now — served in-app
# at /docs (routes/docs.js) so they're readable without internet access.
*.md *.md
!README.md !README.md
!CHANGELOG.md
!DEPLOYMENT.md
!nodejs/api.md
!docs/**/*.md
secrets.js secrets.js
secrets.json secrets.json
*.env *.env
+47
View File
@@ -0,0 +1,47 @@
# Changelog
All notable changes to this project are documented here. Format loosely
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
## [Unreleased]
## [1.1.4] - 2026-07-16
### Added
- **White-label**: `<title>`, the navbar brand text, and the nav logo image were hardcoded "Proxy - Theta 42"/"Dynamic Proxy". Now driven by new `conf.name`/`conf.logo` keys (defaults unchanged). Footer attribution (copyright, `theta42.com` link, GitHub/license links) and favicon are left as-is. Closes [#45](https://github.com/theta42/proxy/issues/45).
## [1.1.3] - 2026-07-16
### Added
- `CHANGELOG.md` (this file), backfilled from the release notes for every tag so far and served in-app at `/docs/changelog`. Closes [theta-env#43](https://github.com/theta42/theta-env/issues/43).
## [1.1.2] - 2026-07-16
### Fixed
- **Air-gap**: `DynamicRecord.refreshAll()` called the public-IP resolvers (`api.ipify.org`, `icanhazip.com`, `ifconfig.me`) every 4h on a timer regardless of whether any dynamic DNS records were configured — the one background network call in the repo not actually gated by feature use. Now skips the lookup entirely when there's nothing to refresh.
- Removed the stray, unauthenticated `GET /test` page (a leftover jq-repeat demo) that loaded jQuery + Mustache from external CDNs.
- Removed a dead IE<9-only `html5shim` script tag pointing at a domain that no longer resolves.
### Added
- **In-app documentation**: `GET /docs` and `GET /docs/:slug` render this project's own README, DEPLOYMENT, `api.md`, and `docs/*.md` server-side — readable from the running app with no dependency on GitHub Pages, which requires internet access to view. Public, no auth, rate-limited.
## [1.1.1] - 2026-07-16
### Fixed
- **DuckDNS provider**: adding a DuckDNS provider no longer pushes this host's public IP to the domain's live A/AAAA record as a side effect of token validation. Validation now writes a fixed marker to the TXT record instead, leaving routing untouched. ([#142](https://github.com/theta42/proxy/pull/142))
## [1.1.0] - 2026-07-16
First tagged release. Establishes the `vX.Y.Z` tag convention that the in-app update-check banner polls against going forward.
### Added
- Standalone backup script (`ops/backup.sh`) for deployments not using theta-env's orchestrator — snapshots Redis and `./config`, with retention.
- Admin-only in-app banner that checks GitHub releases every 24h and surfaces available updates.
[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.4...HEAD
[1.1.4]: https://github.com/theta42/proxy/compare/v1.1.3...v1.1.4
[1.1.3]: https://github.com/theta42/proxy/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/theta42/proxy/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/theta42/proxy/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/theta42/proxy/releases/tag/v1.1.0
+10
View File
@@ -106,6 +106,16 @@ COPY nodejs/services ./services
COPY nodejs/utils ./utils COPY nodejs/utils ./utils
COPY nodejs/views ./views COPY nodejs/views ./views
COPY nodejs/public ./public COPY nodejs/public ./public
COPY nodejs/api.md ./api.md
# Documentation, served in-app at /docs (routes/docs.js) so it's readable
# without internet access. README.md/DEPLOYMENT.md land one level above the
# flattened /app (mirrors sso-manager-node's tos.md -> /tos.md convention);
# docs/ mirrors the repo's own top-level docs/ folder.
COPY README.md /README.md
COPY CHANGELOG.md /CHANGELOG.md
COPY DEPLOYMENT.md /DEPLOYMENT.md
COPY docs /docs
# Baked commit hash from the gitinfo stage (see build_info.js). # Baked commit hash from the gitinfo stage (see build_info.js).
COPY --from=gitinfo /commit.txt ./.build_commit COPY --from=gitinfo /commit.txt ./.build_commit
+2
View File
@@ -17,6 +17,8 @@ proxy serves them over TLS with auto-renewing certs and no downtime on changes.
> the proxy and the SSO find each other without manual config. > the proxy and the SSO find each other without manual config.
**Documentation:** [https://theta42.github.io/proxy/](https://theta42.github.io/proxy/) **Documentation:** [https://theta42.github.io/proxy/](https://theta42.github.io/proxy/)
([CHANGELOG.md](CHANGELOG.md) for what changed in each release) — also
readable from the running app itself at `/docs`, no internet access required.
## Screenshots ## Screenshots
+5
View File
@@ -77,6 +77,11 @@ app.use('/__proxy_auth', require('./routes/host_auth'));
// Routes for front end content. // Routes for front end content.
app.use('/', require('./routes/render')); app.use('/', require('./routes/render'));
// 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'));
// Routes for API // Routes for API
app.use('/api', require('./routes/api')); app.use('/api', require('./routes/api'));
+2
View File
@@ -2,6 +2,8 @@
// Using https://github.com/simpleworkjs/conf to handle configuration // Using https://github.com/simpleworkjs/conf to handle configuration
module.exports = { module.exports = {
name: "Dynamic Proxy", // displayed in the UI
logo: "/static/img/theta42.svg", // shown in the nav; point at your own file under public/ (or an absolute URL) to white-label
userModel: 'redis', // pam, redis, ldap userModel: 'redis', // pam, redis, ldap
ldap: { ldap: {
url: 'ldap://192.168.1.55:389', url: 'ldap://192.168.1.55:389',
+7 -6
View File
@@ -67,14 +67,15 @@ class DuckDns extends DnsApi{
} }
// No API to enumerate owned subdomains, so the operator supplies them. // No API to enumerate owned subdomains, so the operator supplies them.
// This call both validates the token and, as a side effect, syncs each // This call validates the token by writing a fixed marker to the TXT
// domain's A/AAAA record to this host's current public IP if the token // record only -- never `ip`/`ipv6` -- so adding/validating a provider
// is valid (DuckDNS auto-detects the caller's IP when `ip` is omitted) // never changes where the domain's A/AAAA records actually point. (An
// — the same thing an operator would need to do anyway when pointing a // earlier version omitted `ip` here, which made DuckDNS auto-detect and
// fresh DuckDNS domain at this proxy. // apply this host's public IP as a side effect of validation; that's
// exactly what this call must not do.)
async listDomains(){ async listDomains(){
let labels = this.subdomains.split(',').map(d => this.__normalizeLabel(d.trim())).filter(Boolean); let labels = this.subdomains.split(',').map(d => this.__normalizeLabel(d.trim())).filter(Boolean);
await this.update(labels.join(','), {}); await this.update(labels.join(','), {txt: 'theta42-proxy-validated'});
return labels.map(label => ({domain: `${label}.duckdns.org`})); return labels.map(label => ({domain: `${label}.duckdns.org`}));
} }
+10 -2
View File
@@ -76,8 +76,17 @@ class DynamicRecord extends Table{
} }
} }
// Resolve the public IP once, then reconcile every record to it. // Resolve the public IP once, then reconcile every record to it. Checked
// BEFORE the public-IP lookup: on a stock install with zero dynamic
// records configured, this runs on a timer regardless (services/dynamic_dns.js)
// -- without this guard it would still reach out to the public-IP
// resolvers (utils/public_ip.js) every cycle for nothing, which is
// exactly the kind of always-on external call an air-gapped deployment
// can't have.
static async refreshAll(){ static async refreshAll(){
let records = await this.listDetail();
if(!records.length) return {count: 0};
let ip; let ip;
try{ try{
ip = await getPublicIp(); ip = await getPublicIp();
@@ -86,7 +95,6 @@ class DynamicRecord extends Table{
return {error: error.message}; return {error: error.message};
} }
let records = await this.listDetail();
for(let record of records){ for(let record of records){
await record.apply(ip); await record.apply(ip);
} }
+15 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "proxy-api", "name": "proxy-api",
"version": "1.1.0", "version": "1.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "proxy-api", "name": "proxy-api",
"version": "1.1.0", "version": "1.1.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
@@ -25,6 +25,7 @@
"jquery": "^4.0.0", "jquery": "^4.0.0",
"ldapts": "^8.1.8", "ldapts": "^8.1.8",
"linux-sys-user": "^1.2.0", "linux-sys-user": "^1.2.0",
"marked": "^9.1.6",
"model-redis": "^1.5.0", "model-redis": "^1.5.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
@@ -1410,6 +1411,18 @@
"integrity": "sha512-TyPFnk3kp9kplKPjWtYYbezYzj+Xe47bGu3YZs2Jg3xxLUw/ny0AHL252jpR1c8q32vIQxWK8qLpFZ+qHHC0MQ==", "integrity": "sha512-TyPFnk3kp9kplKPjWtYYbezYzj+Xe47bGu3YZs2Jg3xxLUw/ny0AHL252jpR1c8q32vIQxWK8qLpFZ+qHHC0MQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/marked": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
"integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxy-api", "name": "proxy-api",
"version": "1.1.0", "version": "1.1.4",
"private": true, "private": true,
"author": [ "author": [
{ {
@@ -36,6 +36,7 @@
"jquery": "^4.0.0", "jquery": "^4.0.0",
"ldapts": "^8.1.8", "ldapts": "^8.1.8",
"linux-sys-user": "^1.2.0", "linux-sys-user": "^1.2.0",
"marked": "^9.1.6",
"model-redis": "^1.5.0", "model-redis": "^1.5.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
+81
View File
@@ -0,0 +1,81 @@
'use strict';
const fs = require('fs');
const path = require('path');
const router = require('express').Router();
const {rateLimit} = require('express-rate-limit');
const {marked} = require('marked');
const conf = require('@simpleworkjs/conf');
const buildInfo = require('../utils/build_info');
// Public, unauthenticated, and reads from disk on every request -- throttle
// per IP so it can't be used to hammer the filesystem (mirrors the pattern
// in routes/auth.js/routes/host.js), generous since this is just docs.
const docsLimiter = rateLimit({
windowMs: 60 * 1000,
max: 120,
standardHeaders: true,
legacyHeaders: false,
message: {name: 'TooManyRequests', message: 'Too many requests, please try again later.'},
});
const values = {
title: conf.environment !== 'production' ? `dev` : '',
titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '',
name: conf.name,
logo: conf.logo,
...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.
const DOCS = {
overview: {title: 'Overview', file: path.join(__dirname, '../../README.md')},
changelog: {title: 'Changelog', file: path.join(__dirname, '../../CHANGELOG.md')},
deployment: {title: 'Deployment', file: path.join(__dirname, '../../DEPLOYMENT.md')},
api: {title: 'API Reference', file: path.join(__dirname, '../api.md')},
installation: {title: 'Installation', file: path.join(__dirname, '../../docs/installation.md')},
architecture: {title: 'Architecture', file: path.join(__dirname, '../../docs/architecture.md')},
docker: {title: 'Docker', file: path.join(__dirname, '../../docs/docker.md')},
contributing: {title: 'Contributing', file: path.join(__dirname, '../../docs/contributing.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(docsLimiter);
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;
+2 -3
View File
@@ -9,6 +9,8 @@ const buildInfo = require('../utils/build_info');
const values ={ const values ={
title: conf.environment !== 'production' ? `dev` : '', title: conf.environment !== 'production' ? `dev` : '',
titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '', titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '',
name: conf.name,
logo: conf.logo,
...buildInfo, ...buildInfo,
} }
@@ -80,7 +82,4 @@ router.get('/login/*splat', async function(req, res, next) {
res.render('login', {...values, redirect: req.query.redirect}); res.render('login', {...values, redirect: req.query.redirect});
}); });
router.get('/test', async function(req, res, next) {
res.render('test', {...values, redirect: req.query.redirect});
});
module.exports = router; module.exports = router;
+3
View File
@@ -10,6 +10,9 @@
<a href="https://github.com/theta42/proxy/blob/master/LICENSE" target="_blank" class="text-light">MIT License</a> <a href="https://github.com/theta42/proxy/blob/master/LICENSE" target="_blank" class="text-light">MIT License</a>
</span> </span>
<span class="d-flex align-items-center gap-3"> <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/proxy" target="_blank" class="text-light text-decoration-none"> <a href="https://github.com/theta42/proxy" target="_blank" class="text-light text-decoration-none">
<i class="fa-brands fa-github"></i> GitHub <i class="fa-brands fa-github"></i> GitHub
</a> </a>
+26
View File
@@ -0,0 +1,26 @@
<%- 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') %>
+31
View File
@@ -0,0 +1,31 @@
<%- 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 markdown-body">
<%- docHtml %>
</div>
</div>
</div>
</div>
<%- include('bottom') %>
-74
View File
@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<!-- need to load jq-query cdn or file. -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<!-- need to load mustache cdn or file. -->
<script src="
https://cdn.jsdelivr.net/npm/mustache@4.2.0/mustache.min.js
"></script>
<!-- need to load jq-repeat cdn or file -->
<script type="text/javascript" src='/static/lib/js/jq-repeat_new.js'></script>
<script>
//on document ready. the logic would execute.
$(document).ready(function () {
$.scope.toDo.__setPut(function($el, item, list){
$el.slideDown('slow');
})
// $.scope.toDo.__setUpdate(function($el, $render, item, list){
// $el.fadeOut(2000, function() {
// $(this).html($render.html()).fadeIn(2000);
// });
// });
// $.scope.toDo.__setUpdate(function($el, $render, item, list){
// $el.animate({'opacity': 0}, 400, function(){
// $(this).html($render.html()).animate({'opacity': 1}, 400);
// });
// });
$.scope.toDo.__setUpdate(function($el, $render, item, list){
$el.slideUp(function(){
$(this).replaceWith($render)
$render.slideDown()
})
});
$.scope.toDo.push({ item: "Get milk", done: "Yes" }); // 0
$.scope.toDo.push({ item: "Do laundry", done: "No" }); // 1
//should take array id or array key
// - **howMany** _Type: Number_
// Number of repeat objects that will be removed. If there are non to be removed, it is not required to use this argument.
// - **update** _Type: Array_
// This is the array of repeat objects to add. If there are none to this is not required.
//remove works
// $.scope.toDo.splice("Get milk" , "1")
//update
$.scope.toDo.splice(-1,0, { item: "Get Bread", done: "Yes" })
});
</script>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul>
<li jq-repeat="toDo" jq-repeat-index="item" style="display:none;"><span class="item">{{ item }}</span>: {{ done }}</li>
</ul>
</body>
</html>
+2 -8
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Proxy - Theta 42 <%- title %></title> <title><%- name %> <%- title %></title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<!-- CSS are placed here --> <!-- CSS are placed here -->
@@ -24,17 +24,11 @@
<script type="text/javascript" src="/static-modules/moment/moment.js"></script> <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/lib/js/app-base.js"></script>
<script type="text/javascript" src="/static/js/app.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> </head>
<body> <body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">Dynamic Proxy <%- titleIcon %></a> <a class="navbar-brand" href="#"><img src="<%- logo %>" height="28" class="me-2" alt=""><%- name %> <%- titleIcon %></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
+3
View File
@@ -18,6 +18,9 @@
// theta-env orchestrator (setup.sh) and ignored by the app. // theta-env orchestrator (setup.sh) and ignored by the app.
module.exports = { module.exports = {
name: 'Dynamic Proxy', // shown in the UI
logo: '/static/img/theta42.svg', // nav image; point at your own file under public/ to white-label
// OpenID Connect — point at your SSO Manager. Issuer + authorization/ // OpenID Connect — point at your SSO Manager. Issuer + authorization/
// endSession are browser-facing URLs; token/userinfo can be the internal // endSession are browser-facing URLs; token/userinfo can be the internal
// URL if the SSO is on the same docker network (avoids a TLS hairpin). // URL if the SSO is on the same docker network (avoids a TLS hairpin).