Commit Graph

62 Commits

Author SHA1 Message Date
wmantly 6cd3a5bc58 Update jq-repeat to 2.1.0; fix removed __setPut/__setTake API
jq-repeat 2.1.0 (release notes: https://github.com/wmantly/jq-repeat/releases/tag/v2.1.0)
brings real fixes (throttled-update race conditions, sorted-list
reverse() leaking elements, nested-scope isolation) and a few
behavior changes. Audited every usage in this repo against the
changelog before upgrading:

- push()/unshift() now return the new array length -- every call
  site in this repo is a bare statement, none consume the return
  value. No risk.
- update() is now trailing-edge throttled (~50ms) even on the first
  call, not just rapid subsequent ones -- no code in this repo reads
  DOM/item state immediately after calling update(), so no risk here
  (unlike sso-manager-node's companion PR, which needed a fix).
- jr-order-reverse and nested jq-repeat templates: not used anywhere
  in this repo.

Real breakage found and fixed: users.ejs/groups.ejs/permissions.ejs
called $.scope.X.__setPut(fn)/__setTake(fn) as setter METHODS -- that
API is gone in 2.1.0. Insert/remove hooks are now set via direct
property assignment ($.scope.X.__put = fn), per the current README.
Verified live (real dev server + Playwright): before the fix, all
three pages threw "__setTake is not a function" and the
insert/remove row animations were broken; after, zero errors and the
hooks fire correctly.
2026-07-16 18:26:02 -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 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 1df9916c91 Add standalone backup script and admin update-check banner
ops/backup.sh snapshots Redis (BGSAVE, dynamic RDB path lookup) and
./config for standalone deployments, with retention. A background
service polls GitHub releases every 24h and surfaces an admin-only
banner in the UI when a newer version is published.
2026-07-15 22:32:31 -04:00
wmantly ad2cacf094 Make basic auth and SSO mutually exclusive per host; fix silently-broken validation errors
- Auth tab is now a single choice (Off / Basic auth / SSO) instead of two
  independent toggles that could both be on at once, which made it
  ambiguous which gate actually protected a request. Enforced both in the
  UI and server-side (POST/PUT), accounting for partial PUT updates against
  the existing record.
- Add per-user basic-auth management (change password, delete) so an admin
  no longer has to blow away and retype the whole user list to remove or
  rotate one account.
- Fix: `Model.errors.ObjectValidateError(...)` is a constructor and was
  being called without `new` everywhere in this codebase. Without `new`,
  `this` inside it was the module's shared `errors` object (mutated in
  place) and the call evaluated to `undefined` — so every
  `throw Model.errors.ObjectValidateError(...)` actually threw `undefined`,
  which Express's `next(undefined)` treats as "no error" and silently
  falls through to the catch-all 404 handler. Every host/user/group/
  permission/dns-provider validation error (bad hostname, bad IP, etc.) was
  showing a confusing "Page not found" instead of the real message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 00:41:16 -04:00
wmantly 3f4f98ec4e Unify nav: drop Profile nav item, fold API Tokens into Profile page
- Nav bar already showed the logged-in user's name linking to /profile
  (cl-username); just removed the separate "Profile" and "API Tokens"
  nav items now that Profile covers both.
- Merge api_tokens.ejs into profile.ejs as a section below the existing
  profile card. /api-tokens 301-redirects to /profile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 23:44:05 -04:00
wmantly 8aab9c7673 Wrap unresponsive tables in table-responsive (#131)
permissions.ejs (6 columns), users.ejs (3 columns incl. an inline
password-change form), and profile.ejs's domain-permissions table had
no .table-responsive wrapper, so on narrow/mobile viewports they'd
either overflow the page horizontally or force zoomed-out, unreadable
text instead of scrolling within the table. hosts.ejs already had the
wrapper — these three didn't.

Verified: EJS compiles, npm test 192/192 pass, and fetched each route
from a running instance to confirm the wrapper is present in the
served HTML.
2026-07-14 21:17:33 -04:00
wmantly 9d34ff96e2 Add a footer, move GitHub link out of the nav (#130)
proxy had no page footer at all — no copyright, no license link, no
build/version info — unlike sso-manager-node, which already had one
(now cleaned up in a matching change there). Bring the two in line:

- Added a footer to bottom.ejs: theta42 logo/link, "© <year> theta42"
  with an MIT License link, a GitHub link, and build version/hash.
- Moved the GitHub icon link out of the top nav (where it competed
  with actual navigation items) and into the new footer.
- Added nodejs/utils/build_info.js (buildVersion/buildHash/buildYear,
  read once from package.json + git) and wired it into routes/render.js
  so every page has the values the footer needs. Mirrors the same
  helper added to sso-manager-node in the matching cleanup there.
- Copied the theta42.svg logo into nodejs/public/img/ (previously only
  present in sso-manager-node) so both apps' footers render identically.

Verified by rendering top+bottom with the real ejs package: no
template errors, GitHub link present exactly once (in the footer, not
the nav), logo and MIT License link present. npm test: 192/192 pass
(unaffected — view-only + a new leaf utility module).
2026-07-14 20:55:11 -04:00
wmantly a9dfebc481 Allow single-label hostnames as a Host target (#126)
The target ("ip") field validator required at least two dot-separated
labels, rejecting legitimate single-label hostnames like Docker Compose
service names ("sso-manager"), /etc/hosts entries, or anything resolved
via a search domain. This was enforced identically client-side
(public/lib/js/val.js) and server-side (utils/hostname_validate.js,
routes/host.js), so there was no way to set one through the UI or API
— only by writing to the Host model directly, bypassing validation
entirely (which is how theta-env's setup.sh registers sso-manager as a
target today, working only because it calls Host.create() directly).

Relax HOSTNAME in both places to accept either a bare single label or
the existing dotted-FQDN pattern. Flips the one existing test that
codified the old behavior (isValidHostname('localhost') was asserted
false) and adds coverage for the reported case.
2026-07-14 01:12:02 -04:00
wmantly 8dcecbcfa2 Fix api-tokens date display + quiet authIO no-token log (#120)
- api_tokens.ejs: created_on/last_used_on come back from Redis as strings
  (model-redis only coerces fields with an explicit `type`), so `new Date(ms)`
  yielded "Invalid date". Use `moment(ms, "x")` (the hosts.ejs/dns.ejs
  precedent) which parses a numeric string-or-number as a Unix-ms timestamp.
- api_tokens.ejs: `isExpired` is a class getter not serialized to the client
  JSON, so the "expired" badge never showed — compute expiry in the view via
  `Date.now() > Number(expires_at)`. Also guard the `last_used_on: 0` / falsy
  case (string "0" is truthy) so unset timestamps render "—" not "1970".
- middleware/auth.js: authIO did `checkToken(socket.handshake.auth.token || 0)`,
  so any socket connect without a token (login page, pre-login) did an
  `AuthToken.get(0)` lookup and logged a noisy `EntryNotFound` trace. Guard:
  reject the socket with a generic 401 when there's no token (behavior-
  preserving — unauth sockets were already rejected; just no Redis lookup / 404).
- dns_provider.js: drop a stray `console.log('currentDomains:', ...)` debug
  line in updateDomains() (unrelated, noticed while investigating).

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-12 17:41:57 -04:00
wmantly a9a48c3445 Add self-service API tokens (PATs) with UI + Bearer auth (#119)
Personal access tokens so scripts/CI can call the management API without an
OIDC browser session. Each logged-in user mints their own token; it
authenticates as the creator (groups snapshotted at mint, mirroring the proxy's
browser AuthToken), and the existing authz layer (Permission.effectiveFor /
roles.resolveEffective) applies unchanged. Local groups and owned-domain rights
are recomputed live; only SSO/LDAP group membership is the mint-time snapshot.

- models/api_token.js: new ApiToken model (prx_<id>_<secret> format; id is the
  lookup key, secret bcrypt-hashed + isPrivate, shown once). add()/rotate()/
  authenticate(); optional expires_at; best-effort last_used_on; groups
  snapshot. No _ttl (persists). Deliberately NOT wrapped in ModelPs (so the
  last_used_on write on the auth path doesn't spam the socket).
- routes/api_token.js: self-service CRUD (list/get/update/delete/rotate),
  owner-scoped (created_by === reqUsername(req), 403 otherwise).
- middleware/auth.js + models/auth.js: accept `Authorization: Bearer prx_...`
  (precedence over the auth-token session header). Builds a synthetic req.token
  that satisfies the only three req.token reads (auth.js .user/.groupsArray,
  authz.js reqUsername .created_by) so the authz layer works unchanged.
  checkApiToken collapses every failure to one generic 401 (no leak).
- views/api_tokens.ejs + routes/render.js (GET /api-tokens): self-service page
  (forceLogin, no admin gate) — create (token shown once), rotate, revoke.
- views/top.ejs: "API Tokens" nav entry visible to all logged-in users.
- public/js/app.js: app.apiToken client module.
- DEPLOYMENT.md + docs/docker.md: API tokens section.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-12 17:12:38 -04:00
wmantly c98214cc76 Host modal: Authentication tab, wildcard-child default, allow-list autocomplete
- Default the "Parent Wildcard" challenge type when a wildcard parent exists.
- Split Authentication (basic auth + SSO) into its own tab; Access keeps IP
  allow/deny.
- Add GET /api/host/auth-suggestions (authenticated host editors, not just
  admins) and datalist-backed "type to search + Add" pickers for the SSO
  allowed-users/groups lists.

Verified in a browser (tab present, datalist populated, picker appends deduped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:24:29 -04:00
wmantly 774d8e815e Host form: redesign as a tabbed modal
Replace the dense always-open add panel + inline edit card with a single
Bootstrap modal (shared by Add and Edit) organized into tabs: General, TLS &
Wildcard, Traffic, Headers, Access (IP + basic auth + SSO). Adds per-field
explanations, a full-width proxy list, and an "Add host" button. Preserves all
field names/ids, the challenge-type detection JS, and formAJAX wiring; drops the
form-clone edit mechanism in favor of populating the one modal. Verified in a
browser (add + edit, tab navigation, field population).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:10:54 -04:00
wmantly 3e5590288a Per-host HTTP basic auth (#57)
Adds opt-in basic auth per Host, following the existing per-host controls
pattern:
- Host fields basicauth_enabled / basicauth_realm / basicauth_users
  ({user: base64(sha1(pw))}). Credentials are parsed to plaintext by the pure
  host_features normalizer and hashed at the route layer (utils/basicauth.js),
  so plaintext never reaches Redis.
- ops/nginx_conf/hostfeatures.lua enforces it in access phase: verifies the
  Authorization header against base64(sha1(password)), fails closed with a 401
  WWW-Authenticate challenge.
- hosts.ejs gains an enable toggle, realm, and a username:password textarea
  (passwords never echoed back; blank keeps the current set).

Unit tests cover hashing (matches the htpasswd {SHA} vector), credential
parsing, and normalization. Note: the Lua path needs verification on a live
OpenResty box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:47:08 -04:00
wmantly d1586b4d5a Fix user creation and password policy (#48)
Root cause of "can't log in with new credentials": routes/user.js POST called
User.add, which doesn't exist on the redis User model (it has create) — so every
API-created account threw and was never persisted. Switch to User.create and make
the Add button a submit.

Replace the broken password rule (rejected strong "@123Caplowercase", accepted
weak "lowercase1") with a clear policy in utils/password_policy.js: >= 8 chars and
either 12+ chars or 3-of-4 character classes. Enforced server-side on create and
password changes, mirrored in public/lib/js/val.js, with unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:38:09 -04:00
wmantly 6d8dc45209 Validate host/target fields (hostname or IP; host allows */** wildcards)
Backend (utils/hostname_validate.js, enforced in routes/host.js on create/update):
- host: IPv4 or a wildcard pattern whose labels may be normal, "*" (one
  fragment) or "**" (any depth, incl. a bare "**" catch-all) — matching
  Host.lookUp. Lowered Host.host min length to 1 so "**"/"*" pass the model.
- target (ip): IPv4 or a strict hostname, no wildcards.
- Both reject scheme, "/", ":" and whitespace; 422 with per-field keys.

Frontend (val.js) mirrors the rules: host/target validators + hosts.ejs fields
point at them. Unit tests in test/unit/hostname_validate.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:18:47 -04:00
wmantly 2acc3644c4 Permissions: rename Grants, add wildcards, local groups, profile
- Rename Grant -> Permission end-to-end (model, routes, view, frontend,
  bootstrap) and add an idempotent redis migration for existing records.
- utils/roles.js: glob domain matching (* = one label, ** = any depth) against
  the full host; authz passes the full hostname.
- Local groups: LocalGroup model + admin routes/UI; membership merged into
  Permission.effectiveFor so app groups behave like SSO groups.
- Subject autocomplete via GET /api/permission/subjects (users + derived groups).
- User profile page (/profile) and username in the navbar; /api/user/me now
  returns merged/local/external groups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:54:15 -04:00
wmantly 83e9753b18 Dynamic DNS UI: fix blank columns, redesign the record list
The Host and Last-updated columns were blank because they relied on a jq-repeat
parseData hook that the rest of the app doesn't actually use for display (working
rows derive dates from the .momentFromNow class, not parseData).

- Derive fqdn server-side via DynamicRecord.toJSON so it flows through both the
  REST list and websocket payloads; the template uses {{fqdn}} directly.
- Render last-updated with the .momentFromNow class (data-date) like the rest of
  the app instead of a parseData-computed string.
- apply() now clears last_status on success so the UI only surfaces real errors.
- Redesign the section: prominent public-IP badge, cleaner add form, and a
  Bootstrap list-group of records (fqdn, IP badge, "updated N ago", inline error)
  with outline refresh/remove buttons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 00:32:21 -04:00
wmantly a2d194f855 Add dynamic DNS: keep A records pointed at the current public IP
For deployments on WAN DHCP, operators can declare A records in the DNS section
that the app updates to this box's current public IP every 4 hours (and
immediately on create).

- utils/public_ip.js: getPublicIp() queries external echo services (ipify +
  fallbacks, configurable) with pure isIPv4/extractIp helpers.
- utils/dns_records.js: pure planARecordUpdate() reconciliation decision.
- models/dns_provider.js: Domain.upsertARecord(name, ip) — provider-agnostic
  upsert via getRecords + deleteRecordById + createRecord (createRecord alone is
  not a reliable cross-provider upsert). Apex ('@') handling added to each
  provider (CloudFlare uses the domain name, Porkbun an empty name, DigitalOcean
  '@') via a new DnsApi.apexName().
- models/dynamic_record.js: DynamicRecord model (deterministic id per host,
  apply()/refreshAll()), registered + ModelPs-wrapped for live UI updates.
- services/dynamic_dns.js + conf: 4h scheduler mirroring host_scheduler.
- routes/dns.js: /dynamic CRUD + /dynamic/ip, gated to domain managers/admins.
- views/dns.ejs: "Dynamic A Records (WAN IP)" card with add form + list.
- test/unit/dynamic_record.test.js: public-IP parsing + reconciliation logic.

Verified end-to-end against a live Porkbun domain (create, idempotent, IP-change,
cleanup) plus unit suite (111 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 23:55:56 -04:00
wmantly 762b46a5fa Merge branch 'master' into fix/porkbun-domain-sync-zoneid 2026-07-10 23:39:17 -04:00
wmantly 259f1350a3 API issue 2026-07-10 23:36:51 -04:00
wmantly 6092468901 Add per-host reverse-proxy controls (rate limit, cache, headers, IP ACL)
Every proxied request flows through one shared OpenResty location whose
behavior is chosen at request time from the host's Redis hash. Add per-host
controls as new Host fields enforced in Lua rather than static nginx config
(which can't key off a per-request variable):

- Rate limiting: per-client-IP token bucket via resty.limit.req
  (ratelimit_enabled/rate/burst), backed by a new `ratelimit` shared dict.
- Response caching: opt-in per host via a global proxy_cache zone gated by
  $skip_cache (respcache_enabled). Off by default; upstream Cache-Control
  still honored.
- Custom/security headers: req_headers (upstream) + resp_headers (client) and
  hsts_enabled, applied in access/header_filter phases.
- IP allow/deny CIDR lists via resty.ipmatcher (deny wins; non-empty allow is
  default-deny).

New ops/nginx_conf/hostfeatures.lua holds the enforcement; proxy.conf's
access_by_lua string becomes a block that calls it, plus a header_filter block.
nodejs/utils/host_features.js is the pure, unit-tested normalize/validate layer
(header/CIDR parsing, range clamping, injection-safe values) applied in
routes/host.js and mirrored by the hosts.ejs edit form. install.sh gains the
ipmatcher rock, the cache dir, and the hostfeatures.lua symlink.

Per-host cache TTL is intentionally deferred (global default only) — see the
plan's limitations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 22:10:55 -04:00
wmantly 10abd36340 Add OIDC login and per-domain authorization
Authentication previously implied full authorization: any valid token
could manage every host, DNS provider, domain, and user. This adds SSO
login and a per-domain rights model.

OIDC login (authorization_code + PKCE):
- conf.oidc + conf.auth blocks; clientSecret in (gitignored) secrets.js.
- utils/oidc.js (state/PKCE, code exchange, userinfo) using global fetch.
- models/oidc_state.js: short-lived state store, auto-expiring via
  model-redis 1.5 per-key TTL.
- routes/auth.js: GET /auth/oidc/start + /auth/oidc/callback; JIT-provisions
  a local user, mints an AuthToken carrying the SSO groups, hands the token to
  the browser via a URL fragment. "Log in with SSO" button on the login page.

Authorization (groups + app overrides, per-domain, with ownership):
- models/grant.js + utils/roles.js (pure, unit-tested): effective rights from
  conf.auth (admin users/groups, group->role map), Grant records
  (user|group -> global|domain -> viewer|manager|admin), and ownership
  (created_by). Roles rank admin > manager(owner) > viewer.
- AuthToken stores session groups; middleware/auth.js exposes req.groups.
- middleware/authz.js: requireAdmin, requireDomainRole(minRole, resolveDomain),
  filterViewable. Applied across routes: host mutations need manager on the
  host's domain; reads are filtered to visible domains; DNS providers, user
  management, and grant management are global-admin-only; certs need viewer.
- routes/grant.js: admin CRUD for grants. Anti-lockout via conf.auth.adminUsers
  plus migrations/grant_bootstrap.js.

Frontend: /me returns effective rights; nav gates Users/Grants to admins;
grants management page; OIDC token-fragment handling in app-base.js.

Tests: utils/roles and utils/oidc unit-tested (no redis); wired into the test
scripts. Full suite 89 pass. Also verified end-to-end against redis (grant
resolution, middleware allow/deny/403, list filtering) and the OIDC pure flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:17:05 -04:00
wmantly 56c2fb1a5c Fixed issue with matching wild card 2026-07-10 00:13:40 -04:00
wmantly 2c32ec0f3a Add wildcard_matchAny routing mode for wildcard hosts
A *.example.com wildcard host now chooses between two routing modes:
- wildcard_matchAny=false (default): only subdomains explicitly defined
  in redis route; undefined subdomains get no match (406)
- wildcard_matchAny=true: any subdomain catches-all to the wildcard
  parent host, preserving the previous behavior

The gate lives in the host_lookup socket service, which is only reached
for domains missing a direct redis entry, so defined children and
**-style hosts are unaffected. Adds the matching-mode selector to the
host add/edit form, shown for wildcard hosts.

Note: existing wildcard hosts have no wildcard_matchAny field and so
default to the stricter "only defined" mode until re-saved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:47:21 -04:00
wmantly bcc777ccdf Delete certs from redis 2026-02-25 22:21:16 -05:00
wmantly 1bacc17a12 Fixed host put on frontend 2026-02-25 19:45:09 -05:00
wmantly 21ff1b0bb5 DNS route 2026-02-25 12:11:31 -05:00
wmantly 8692b5f3d7 Added fav icon 2025-12-31 19:08:28 -05:00
wmantly 5dd07edf1a Moved jq-repeat to use NPM package 2025-12-31 16:01:34 -05:00
wmantly ef3fffdef2 Updated DNS-01 validation 2025-12-31 16:00:21 -05:00
wmantly f9d3ae5524 Improved mobile UI 2024-08-15 18:48:07 -04:00
wmantly 496bff53c8 Fixes mobile issue with nav bar 2024-08-15 18:47:30 -04:00
wmantly b27e1b1617 Added relations to redis ORM 2024-08-13 15:18:47 -04:00
wmantly 35d3a399e3 More UI stuff 2024-08-12 13:53:37 -04:00
wmantly e86de04a69 Better UI for DNS 2024-08-11 22:57:18 -04:00
wmantly cb87f22d98 DNS API error message 2024-08-11 12:24:42 -04:00
wmantly 3df3341235 DNS stuff 2024-08-10 23:07:32 -04:00
wmantly f44284685f Fixed nginx conf issue 2024-08-07 21:34:03 -04:00
wmantly 852ad67430 Fixed login UI and redirect 2024-08-07 14:53:53 -04:00
wmantly 0547e8efdf Fixed issue with form ajax 2024-08-07 14:52:02 -04:00
wmantly 2a4cd8dba6 Renamed functions to match 2024-08-07 14:51:29 -04:00
wmantly 41919db8f8 wild card UI done 2024-08-06 17:52:00 -04:00
wmantly 84db245f4a fixed issue with redis models 2024-08-06 13:11:55 -04:00
wmantly e38504457c WS fixes 2024-07-31 23:30:21 -04:00
wmantly fb9a19d5f0 Changed .add to .create 2024-07-31 20:02:51 -04:00
wmantly 75a486251d UI fixes 2024-07-31 19:43:01 -04:00
wmantly a173a48e43 Merge branch 'certs' of github.com:theta42/proxy into certs 2024-07-31 19:03:11 -04:00
wmantly 441e372987 Added socket.io 2024-07-31 19:01:55 -04:00
noot 5cfc55c37a search bar functionality 2024-08-01 03:09:09 +08:00