feat(portal): add service drawers and employee portal preview

This commit is contained in:
DeCentN2Madness committed 2026-10-30 00:03:21 -07:00
1 parent ff80ee516f
commit e97f0ea9b4
39 files changed
+1446 -130

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
# Employee portal compatibility and private migration
Recorded 2026-10-30. Browser-side shared-code decryption is approved only for
local compatibility development. It is not authentication, individual revocation,
a secure session or a HIPAA compliance determination. Resources need separate
server authorization before cutover.
## Source and private collection
Read-only legacy footer source:
`/home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt`.
The original stays outside Git as rollback evidence. No access code was available
to the agent. The exact resource count and real content compatibility remain
unverified. Do not infer the earlier approximate 43 links as a measured count.
From `www`, in the owner's private interactive terminal:
```sh
node tools/employee-portal-migrate.mjs collect \
/home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt
```
Enter each legitimate view code at its hidden prompt. Never place codes in
arguments, environment variables, shell history or reports. The utility checks
both streams, keeps plaintext in memory, and collects only referenced `/hubfs/`
resources from the verified source origin. Other origins require explicit
review/code allowlisting; redirects are bounded and never inherit credentials.
No Jotform, Google or other vendor forms are fetched.
Private output is `private/employee-portal/`: directories 0700, manifests and
binary resources 0600. This exact subtree is gitignored and outside Astro's
public directory. Filenames are SHA-256 hashes of source URLs, independently
verified by content hashes. The manifest retains original references, labels,
view membership, MIME, bytes, status and delivery/classification decisions.
All new resources default to UNRESOLVED. Publicly reachable does not mean public
approved. Resuming verifies an existing file's content hash; mismatches are not
overwritten. HTML error documents, executable responses, invalid known signatures,
redirects outside approved destinations and resources over 50 MiB are rejected.
The first pass allows only the verified site origin. If a legitimate `/hubfs/`
response redirects to a HubSpot CDN, inspect that exact HTTPS origin first, then
add its origin to the private manifest's `approvedOrigins` list and rerun. Never
approve a redirect based only on a failed download or a broadly shared vendor
domain. Failures remain private review items. The utility prints counts only.
Before every artifact transfer, verify that only `dist` is copied and that it
contains no private manifest, document or real ciphertext unless that ciphertext
has separately been authorized for the local preview. Never archive the whole
working directory as a deployable artifact.
## Approved rewriting
The owner classifies records as PUBLIC-APPROVED, STAFF-INTERNAL,
MANAGEMENT-INTERNAL or UNRESOLVED in the private manifest. Set `deliveryPath`,
`rewriteApproved`, `destinationImplemented`, `destinationVerified`, and (for
internal documents) `destinationAccessControlled` only with actual evidence.
The utility does not create those destinations or infer their approval.
```sh
node tools/employee-portal-migrate.mjs rewrite \
/home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt
```
It reauthenticates both views, changes approved quoted href/src attributes only,
preserves unrelated HTML/labels, and re-encrypts with fresh salts and IVs. It
verifies round-trip decryption and distinct codes before writing new ciphertext
with exclusive filenames. The original source is untouched. CSS/srcset mappings
require separate review and are rejected rather than silently rewritten.
Unresolved links retain their original destination as a recorded dependency,
not a claim of verified destination health. A changed manifest alone never
changes links in the encrypted stream.
## Local preview and content safety
Use a private ciphertext file generated by `collect` or `rewrite`:
```sh
AIA_PORTAL_LOCAL_PREVIEW_APPROVED=true \
AIA_PORTAL_CIPHERTEXT_FILE=/absolute/private/ciphertext-TIMESTAMP.json \
PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test \
PUBLIC_AIA_FORMS_MODE=off PUBLIC_ALLOW_INDEXING=false \
npm run dev -- --host 127.0.0.1
```
The page defaults to locked/unavailable without approved valid input. Indexing
builds cannot include it. The codes never go to a server, storage or URL. No
native form submits; Enter and Unlock use local WebCrypto. Only the matching
stream is displayed; refresh or leaving the page locks it again.
The passive DOM renderer preserves headings, lists, text, tables and safe
HTTPS/mailto/tel links, with no referrer on links. It rejects scripts, inline
handlers, styles, media, embedded forms and unknown tags. Real portal markup
has not been decrypted or reviewed; any removed behavior must be reviewed with
the owner before claiming fidelity. It must not silently become arbitrary
script execution. No real encrypted stream or plaintext is in Git.
Before owner acceptance: validate two real view codes privately, inspect every
resource/category, responsive directories, focus, link semantics and sanitization
impact. Check wrong-code feedback, keyboard unlock, no-JS explanation and refresh.
Production must use a separately reviewed runtime ciphertext delivery mechanism;
this batch intentionally provides only a gated local build input. Browser-delivered
ciphertext is publicly downloadable by anyone able to reach that preview.
+72
View File
@@ -0,0 +1,72 @@
# Inactive SOPS/age preparation, 2026-10-30
The owner selected SOPS, age and sops-nix. This directory is a template, not a
host configuration. The development flake remains unchanged. No real recipient,
key, encrypted secret or server user is supplied. No module is deployed.
Official references inspected for this pass:
- [SOPS documentation](https://getsops.io/docs/)
- [sops-nix installation and secret permissions](https://github.com/Mic92/sops-nix)
SOPS encrypts configuration at rest. sops-nix can supply decrypted values at
runtime under `/run/secrets` with explicit owners and modes. This does not make
browser ciphertext or publicly served documents private.
## Owner setup before activation
1. Supply the actual NixOS topology, host flake, deploy user, persisted key
location, backup owner, HTTPS termination and resource-service identity.
2. Pin a reviewed `github:Mic92/sops-nix` input in that host flake; import
`inputs.sops-nix.nixosModules.sops` and the reviewed template. Do not add the
input to the Astro development flake just to evaluate a deployment sketch.
3. The owner generates independent developer/recovery and VPS age identities in
private terminals. Record only public recipients in the reviewed SOPS
creation rule. Use distinct recipients, never a placeholder recipient. Keep
the VPS private identity on persistent root-controlled storage (0600), outside
`/nix/store`, Git, the public static root and CI artifacts.
4. Encrypt the approved credential material using SOPS with both recipients.
Review recipient access, least privilege and an offline recovery copy. SOPS
encrypted files may enter a deployment repository only after owner approval.
5. Set `aia.portalSecrets` options explicitly, then evaluate the complete host
configuration and test decryption on authorized nonproduction infrastructure.
Runtime consumers use the generated `.path`, not Nix evaluation of plaintext.
6. Decide which service reloads after rotation. Rotate application credentials
as well as SOPS recipients when revoking access: rewrapping alone does not
invalidate an already copied password. Test recovery and key restoration.
## Separate staff and management resource authorization
Keep original documents outside Astro `public`, `dist`, release archives and
CDN roots. Proposed delivery namespaces are `/staff-resources/` and
`/management-resources/`; neither is implemented or considered working here.
Use nginx `auth_request` to an approved authorization service or an equivalent
server-enforced policy with individually revocable identities and staff and
management roles. Staff credentials must not authorize management resources;
management access to staff resources must be an explicit policy. Backend file
locations must be internal, inaccessible through static aliases or alternate
paths. Never trust a browser-provided role header.
A separate shared Basic Auth credential file per namespace can be considered
only as an explicitly approved weaker interim model, not identity management or
automatic role inheritance. The template prepares separate secret inputs without
selecting that access model or installing an identity provider.
Use HTTPS, `Cache-Control: private, no-store`, no shared/CDN caching, and
`Referrer-Policy: no-referrer` for protected delivery. Avoid credentials and
sensitive resource paths in access/error logs; review monitoring and backups.
Authorization failures must fail closed, including at proxy/cache boundaries.
Test direct requests, alternate encodings, role crossover and rollback.
## Distinct boundaries and release checks
- SOPS: deployment secrets at rest and controlled runtime access.
- Portal HTML: shared-code browser decryption, offline-guessable access friction.
- Documents: require server authorization even if URLs are encrypted in the UI.
The compatibility preview cannot grant production portal access. Before cutover,
classify every document, implement and verify its delivery destination, rewrite
and re-encrypt links, test two-view separation, rotate shared codes privately,
check the public artifact for private files, and obtain owner/security approval.
Back up encrypted configuration and documents separately from keys; rehearse
restore and rollback. Revoking a static release cannot retract ciphertext or
files already downloaded by visitors. Site cutover is still unauthorized.
+26
View File
@@ -0,0 +1,26 @@
# INACTIVE TEMPLATE. Not imported by the development flake or any deployed host.
# Requires an owner-reviewed sops-nix NixOS module import in the host flake.
{ config, lib, ... }:
let cfg = config.aia.portalSecrets;
in {
options.aia.portalSecrets = {
enable = lib.mkEnableOption "owner-reviewed portal deployment secrets";
encryptedFile = lib.mkOption { type = lib.types.path; description = "SOPS-encrypted YAML only; never a plaintext file"; };
ageKeyFile = lib.mkOption { type = lib.types.str; description = "Private persisted host age key path outside the Nix store"; };
readerUser = lib.mkOption { type = lib.types.str; description = "Approved resource authorization service user"; };
};
config = lib.mkIf cfg.enable {
assertions = [{ assertion = lib.hasPrefix "/" cfg.ageKeyFile && !(lib.hasPrefix "/nix/store/" cfg.ageKeyFile);
message = "The host age identity must be an absolute runtime path outside the Nix store."; }];
sops.defaultSopsFile = cfg.encryptedFile;
sops.age.keyFile = cfg.ageKeyFile;
sops.age.generateKey = false;
sops.age.sshKeyPaths = [];
sops.gnupg.sshKeyPaths = [];
sops.secrets."portal/staff-credentials" = { owner = cfg.readerUser; mode = "0400"; };
sops.secrets."portal/management-credentials" = { owner = cfg.readerUser; mode = "0400"; };
# Consumers reference config.sops.secrets.<name>.path at runtime.
# Do not use builtins.readFile on a decrypted secret, environment.etc.text,
# PUBLIC_* values, or Astro/Vite imports to transport secrets.
};
}