Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b08eecca9 | |||
| 7c0cb5eabd | |||
| 3f0d6fb438 | |||
| 6cd3a5bc58 | |||
| 88cf5cf281 | |||
| 526545ee68 | |||
| ecf064b9ae | |||
| fd71485960 | |||
| 1c29ba7206 | |||
| 17e9ef2783 | |||
| 983f2a71f0 | |||
| edf60b3e3d | |||
| 1f10d0db14 | |||
| 4bf1768529 | |||
| ed275acbe7 | |||
| 8565f4aa27 | |||
| 8c22d69e44 | |||
| c71b23ef82 | |||
| 7d9c63b049 | |||
| 5a685e9dd7 | |||
| f98c4ec44f | |||
| 8deecdba73 | |||
| 3e90062255 |
+6
-2
@@ -14,8 +14,6 @@ ops/cookbooks/
|
||||
ops/roles/
|
||||
ops/proxy.service
|
||||
|
||||
# Docs site (served via GitHub Pages, not from the image).
|
||||
docs/
|
||||
.github/
|
||||
|
||||
# Git + editor + secrets.
|
||||
@@ -24,8 +22,14 @@ docs/
|
||||
# nodejs/utils/build_info.js), then it's discarded before the final stage.
|
||||
# It never ends up in the final image.
|
||||
.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
|
||||
!README.md
|
||||
!CHANGELOG.md
|
||||
!DEPLOYMENT.md
|
||||
!nodejs/api.md
|
||||
!docs/**/*.md
|
||||
secrets.js
|
||||
secrets.json
|
||||
*.env
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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.5] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
- Bumped `jq-repeat` 2.0.1 -> 2.1.0. Fixed real breakage: `users.ejs`/`groups.ejs`/`permissions.ejs` called the removed `$.scope.X.__setPut(fn)`/`__setTake(fn)` setter-method API; insert/remove row hooks are now set via direct property assignment (`$.scope.X.__put = fn`), matching 2.1.0's API.
|
||||
|
||||
## [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.5...HEAD
|
||||
[1.1.5]: https://github.com/theta42/proxy/compare/v1.1.4...v1.1.5
|
||||
[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
@@ -106,6 +106,16 @@ COPY nodejs/services ./services
|
||||
COPY nodejs/utils ./utils
|
||||
COPY nodejs/views ./views
|
||||
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).
|
||||
COPY --from=gitinfo /commit.txt ./.build_commit
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -77,6 +77,11 @@ app.use('/__proxy_auth', require('./routes/host_auth'));
|
||||
// Routes for front end content.
|
||||
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
|
||||
app.use('/api', require('./routes/api'));
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Using https://github.com/simpleworkjs/conf to handle configuration
|
||||
|
||||
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
|
||||
ldap: {
|
||||
url: 'ldap://192.168.1.55:389',
|
||||
|
||||
@@ -67,14 +67,15 @@ class DuckDns extends DnsApi{
|
||||
}
|
||||
|
||||
// No API to enumerate owned subdomains, so the operator supplies them.
|
||||
// This call both validates the token and, as a side effect, syncs each
|
||||
// domain's A/AAAA record to this host's current public IP if the token
|
||||
// is valid (DuckDNS auto-detects the caller's IP when `ip` is omitted)
|
||||
// — the same thing an operator would need to do anyway when pointing a
|
||||
// fresh DuckDNS domain at this proxy.
|
||||
// This call validates the token by writing a fixed marker to the TXT
|
||||
// record only -- never `ip`/`ipv6` -- so adding/validating a provider
|
||||
// never changes where the domain's A/AAAA records actually point. (An
|
||||
// earlier version omitted `ip` here, which made DuckDNS auto-detect and
|
||||
// apply this host's public IP as a side effect of validation; that's
|
||||
// exactly what this call must not do.)
|
||||
async listDomains(){
|
||||
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`}));
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
let records = await this.listDetail();
|
||||
if(!records.length) return {count: 0};
|
||||
|
||||
let ip;
|
||||
try{
|
||||
ip = await getPublicIp();
|
||||
@@ -86,7 +95,6 @@ class DynamicRecord extends Table{
|
||||
return {error: error.message};
|
||||
}
|
||||
|
||||
let records = await this.listDetail();
|
||||
for(let record of records){
|
||||
await record.apply(ip);
|
||||
}
|
||||
|
||||
Generated
+19
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||
@@ -21,10 +21,11 @@
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"extend": "^3.0.2",
|
||||
"jq-repeat": "^2.0.1",
|
||||
"jq-repeat": "^2.1.0",
|
||||
"jquery": "^4.0.0",
|
||||
"ldapts": "^8.1.8",
|
||||
"linux-sys-user": "^1.2.0",
|
||||
"marked": "^9.1.6",
|
||||
"model-redis": "^1.5.0",
|
||||
"moment": "^2.30.1",
|
||||
"mustache": "^4.2.0",
|
||||
@@ -1374,9 +1375,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jq-repeat": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.0.1.tgz",
|
||||
"integrity": "sha512-ATI25tKQG3uHW8f8XPqBe85JsH4PNGHA/YLy1KgMVeYDoUSf9cqGNBum+4A+Pg1WKh9PA6bYyWfYNsgktwIbSg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.1.0.tgz",
|
||||
"integrity": "sha512-e1OmSWeBEHEtyOhNVysx0bnT5wd6HlZ37JZgPcGPmACJ0K9bXDPq0xOwrM1slQMSTw7FOSNDX+MD6VwvPeeZyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1410,6 +1411,18 @@
|
||||
"integrity": "sha512-TyPFnk3kp9kplKPjWtYYbezYzj+Xe47bGu3YZs2Jg3xxLUw/ny0AHL252jpR1c8q32vIQxWK8qLpFZ+qHHC0MQ==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.5",
|
||||
"private": true,
|
||||
"author": [
|
||||
{
|
||||
@@ -32,10 +32,11 @@
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"extend": "^3.0.2",
|
||||
"jq-repeat": "^2.0.1",
|
||||
"jq-repeat": "^2.1.0",
|
||||
"jquery": "^4.0.0",
|
||||
"ldapts": "^8.1.8",
|
||||
"linux-sys-user": "^1.2.0",
|
||||
"marked": "^9.1.6",
|
||||
"model-redis": "^1.5.0",
|
||||
"moment": "^2.30.1",
|
||||
"mustache": "^4.2.0",
|
||||
|
||||
@@ -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;
|
||||
@@ -9,6 +9,8 @@ const buildInfo = require('../utils/build_info');
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -80,7 +82,4 @@ router.get('/login/*splat', async function(req, res, next) {
|
||||
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;
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
<a href="https://github.com/theta42/proxy/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/proxy" target="_blank" class="text-light text-decoration-none">
|
||||
<i class="fa-brands fa-github"></i> GitHub
|
||||
</a>
|
||||
|
||||
@@ -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') %>
|
||||
@@ -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') %>
|
||||
@@ -60,10 +60,10 @@
|
||||
|
||||
loadUserSuggestions();
|
||||
|
||||
$.scope.LocalGroup.__setTake(function($el){
|
||||
$.scope.LocalGroup.__take = function($el){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(600, function(){ $el.remove(); });
|
||||
});
|
||||
};
|
||||
|
||||
app.subscribe(/^model:LocalGroup:create/, function(data){
|
||||
$.scope.LocalGroup.remove(data.name);
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
|
||||
loadSubjectSuggestions();
|
||||
|
||||
$.scope.Permission.__setTake(function($el, item, list){
|
||||
$.scope.Permission.__take = function($el, item, list){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(600, function(){ $el.remove(); });
|
||||
});
|
||||
};
|
||||
|
||||
// Live updates (model:Permission:*), so adds/removes reflect for everyone.
|
||||
app.subscribe(/^model:Permission:create/, function(data){
|
||||
|
||||
@@ -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>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<!-- 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/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>
|
||||
|
||||
<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">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
for(let user of data.results){
|
||||
$.scope.users.push(user);
|
||||
}
|
||||
$.scope.users.__setPut(function($el, item, list){
|
||||
$.scope.users.__put = function($el, item, list){
|
||||
$el.addClass('bg-success');
|
||||
$el.fadeIn(3000, function(){
|
||||
$el.removeClass('bg-success');
|
||||
});
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
$(document).ready(function(){
|
||||
populateUsers(); //populate the table
|
||||
|
||||
$.scope.users.__setTake(function($el, item, list){
|
||||
$.scope.users.__take = function($el, item, list){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(1000, function(){
|
||||
$el.remove()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
// theta-env orchestrator (setup.sh) and ignored by the app.
|
||||
|
||||
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/
|
||||
// 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).
|
||||
|
||||
Reference in New Issue
Block a user