working auth in new home
This commit is contained in:
parent
5b7fe5a1d2
commit
19678b3b22
@ -4303,6 +4303,12 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.list-action-button--center {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dialog-offset {
|
||||
padding-top: calc(50vh - 400px);
|
||||
}
|
||||
|
||||
14
bootstrap-serve.ts
Normal file
14
bootstrap-serve.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Entry point for the compiled server binary.
|
||||
* Sets FRESH_ROOT so the server finds _fresh/client and _fresh/server next to the binary, then starts Deno.serve.
|
||||
*/
|
||||
import { join, dirname } from "https://deno.land/std@0.224.0/path/mod.ts";
|
||||
|
||||
// Must run before importing the server so the bundle uses this root at load time
|
||||
const binDir = dirname(Deno.execPath());
|
||||
Deno.env.set("FRESH_ROOT", join(binDir, "_fresh"));
|
||||
|
||||
const { default: handler } = await import("./_fresh/server.js");
|
||||
|
||||
const port = Number(Deno.env.get("PORT")) || 8080;
|
||||
Deno.serve({ port }, handler.fetch);
|
||||
57
build-publish.sh
Executable file
57
build-publish.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a publish directory: Linux binary (server) + _fresh for deployment. .env is not copied (publish dir is not recreated).
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PUBLISH_DIR="$(cd "$SCRIPT_DIR/.." && pwd)/publish"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "Building Fresh app..."
|
||||
deno task build
|
||||
|
||||
# So the compiled binary finds _fresh at runtime (not compile-time path)
|
||||
echo "Patching server-entry to use FRESH_ROOT at runtime..."
|
||||
sed -i 's|const root = join(import.meta.dirname, "..");|const root = Deno.env.get("FRESH_ROOT") ?? join(import.meta.dirname, "..");|' _fresh/server/server-entry.mjs
|
||||
|
||||
echo "Ensuring publish directory exists (parent of project)..."
|
||||
mkdir -p "$PUBLISH_DIR"
|
||||
|
||||
echo "Compiling Linux binary (server)..."
|
||||
deno compile -A --no-check --include ./_fresh/client --target x86_64-unknown-linux-gnu -o "$PUBLISH_DIR/server" bootstrap-serve.ts
|
||||
|
||||
echo "Copying _fresh into publish/..."
|
||||
# Full _fresh (server + client assets) so the binary finds favicon, static files, etc.
|
||||
cp -r _fresh "$PUBLISH_DIR/"
|
||||
|
||||
# Create systemd service file (install with: sudo cp ... /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now cyberhybridhub)
|
||||
SERVICE_NAME="cyberhybridhub"
|
||||
SERVICE_FILE="$PUBLISH_DIR/${SERVICE_NAME}.service"
|
||||
cat > "$SERVICE_FILE" << EOF
|
||||
[Unit]
|
||||
Description=CyberHybridHub web app
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$PUBLISH_DIR
|
||||
Environment=PORT=80
|
||||
ExecStart=$PUBLISH_DIR/server
|
||||
Restart=on-failure
|
||||
# Binding to port 80 typically requires root; run 'sudo systemctl start $SERVICE_NAME' when installing
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 644 "$SERVICE_FILE"
|
||||
echo "Created $SERVICE_FILE"
|
||||
|
||||
echo "Installing and reinitializing systemd service..."
|
||||
sudo cp "$SERVICE_FILE" "/etc/systemd/system/"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||
sudo systemctl enable --now "$SERVICE_NAME"
|
||||
echo "Service $SERVICE_NAME is enabled and running (port 80)."
|
||||
|
||||
echo "Done. Contents of $PUBLISH_DIR:"
|
||||
ls -la "$PUBLISH_DIR/"
|
||||
ls -la "$PUBLISH_DIR/_fresh" 2>/dev/null || true
|
||||
@ -6,7 +6,7 @@
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
APP_DIR="/home/nanderson/nathanAndrinoid/WebSites/cyberhybridhub.com/publish"
|
||||
APP_DIR="/home/chh/cyberhybridhub.com/publish"
|
||||
APP_NAME="cyberhybridhub-web"
|
||||
PID_FILE="$APP_DIR/.cyberhybridhub-web.pid"
|
||||
LOG_FILE="$APP_DIR/.cyberhybridhub-web.log"
|
||||
@ -84,13 +84,13 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=nanderson
|
||||
User=chh
|
||||
WorkingDirectory=$APP_DIR
|
||||
ExecStart=/bin/bash -c 'set -a && source $APP_DIR/../.env 2>/dev/null || true && set +a && exec $APP_DIR/server -A'
|
||||
ExecStart=/bin/bash -c 'set -a && source $APP_DIR/.env 2>/dev/null || true && set +a && exec $APP_DIR/server -A'
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=append:$LOG_FILE
|
||||
StandardError=append:$LOG_FILE
|
||||
StandardOutput=append:$APP_DIR/.cyberhybridhub-web.log
|
||||
StandardError=append:$APP_DIR/.cyberhybridhub-web.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
12
deno.json
12
deno.json
@ -3,14 +3,16 @@
|
||||
"tasks": {
|
||||
"check": "deno fmt && deno fmt --check . && deno lint . && deno check",
|
||||
"dev": "vite",
|
||||
"build": "deno install -r --allow-scripts && vite build",
|
||||
"dev:no-vite": "deno run -A dev.ts",
|
||||
"build": "vite build",
|
||||
"build:full": "deno install -r --allow-scripts && vite build",
|
||||
"start": "deno serve -A --port 8080 _fresh/server.js",
|
||||
"update": "deno run -A -r jsr:@fresh/update .",
|
||||
"e2e": "deno run -A npm:playwright test",
|
||||
"e2e:ui": "deno run -A npm:playwright test --ui",
|
||||
"e2e:report": "deno run -A npm:playwright show-report",
|
||||
"db:generate": "deno run -A drizzle-kit generate",
|
||||
"db:migrate": "deno run -A drizzle-kit migrate",
|
||||
"db:migrate": "deno --env -A --node-modules-dir npm:drizzle-kit migrate",
|
||||
"db:push": "deno run -A drizzle-kit push",
|
||||
"db:studio": "deno run -A drizzle-kit studio",
|
||||
"test:auth-callback-db": "./tests/run-auth-callback-db-test.sh"
|
||||
@ -23,10 +25,10 @@
|
||||
"@preact/signals": "npm:@preact/signals@^2.8.0",
|
||||
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.8",
|
||||
"vite": "npm:vite@^7.3.1",
|
||||
"@types/pg": "npm:@types/pg@^8.15.4",
|
||||
"@types/pg": "npm:@types/pg@^8.16.0",
|
||||
"drizzle-kit": "npm:drizzle-kit@^0.31.9",
|
||||
"drizzle-orm": "npm:drizzle-orm@^0.44.7",
|
||||
"pg": "npm:pg@^8.16.3",
|
||||
"drizzle-orm": "npm:drizzle-orm@^0.45.1",
|
||||
"pg": "npm:pg@^8.18.0",
|
||||
"iron": "npm:iron-webcrypto",
|
||||
"$std/": "https://deno.land/std@0.224.0/",
|
||||
"djwt": "https://deno.land/x/djwt@v3.0.2/mod.ts",
|
||||
|
||||
110
dev.ts
Normal file
110
dev.ts
Normal file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env -S deno run -A
|
||||
/**
|
||||
* Dev server without the Vite dev server: build + serve the Fresh app with
|
||||
* inspector for debugging, and rebuild/restart on file changes.
|
||||
* Use this when "deno task dev" (Vite) breaks form redirects or other behavior.
|
||||
*
|
||||
* Run: deno task dev:no-vite
|
||||
* Debug: attach to ws://localhost:9229 (Chrome DevTools, VS Code, etc.)
|
||||
*/
|
||||
|
||||
const PORT = Deno.env.get("PORT") || "8080";
|
||||
const INSPECT = Deno.env.get("INSPECT") !== "false"; // set INSPECT=false to disable
|
||||
const WATCH_DEBOUNCE_MS = 400;
|
||||
|
||||
const WATCH_PATHS = [
|
||||
"routes",
|
||||
"islands",
|
||||
"main.ts",
|
||||
"services",
|
||||
"db",
|
||||
"utils.ts",
|
||||
"utils",
|
||||
"helpers.ts",
|
||||
"client.ts",
|
||||
"assets",
|
||||
"static",
|
||||
];
|
||||
|
||||
async function build(): Promise<boolean> {
|
||||
const c = new Deno.Command("deno", {
|
||||
args: ["task", "build"],
|
||||
cwd: Deno.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
const child = c.spawn();
|
||||
const status = await child.status;
|
||||
return status.success;
|
||||
}
|
||||
|
||||
function startServer(): Deno.ChildProcess {
|
||||
const args = [
|
||||
"serve",
|
||||
"-A",
|
||||
"--port",
|
||||
PORT,
|
||||
"_fresh/server.js",
|
||||
];
|
||||
if (INSPECT) {
|
||||
args.splice(2, 0, "--inspect=0.0.0.0:9229");
|
||||
}
|
||||
const c = new Deno.Command("deno", {
|
||||
args,
|
||||
cwd: Deno.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
return c.spawn();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Building...");
|
||||
if (!(await build())) {
|
||||
console.error("Build failed.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
let serverProcess: Deno.ChildProcess | null = startServer();
|
||||
console.log(
|
||||
`Server running at http://localhost:${PORT} (no Vite dev server).`,
|
||||
);
|
||||
if (INSPECT) {
|
||||
console.log("Debugger: attach to ws://localhost:9229");
|
||||
}
|
||||
console.log("Watching for changes (rebuild + restart on save)...\n");
|
||||
|
||||
let debounceTimer: number | undefined;
|
||||
const watcher = Deno.watchFs(WATCH_PATHS);
|
||||
|
||||
try {
|
||||
for await (const event of watcher) {
|
||||
const path = event.paths[0] ?? "";
|
||||
if (
|
||||
path.includes("node_modules") ||
|
||||
path.includes("/_fresh/") ||
|
||||
path.endsWith(".map")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
debounceTimer = undefined;
|
||||
console.log("\nChange detected, rebuilding...");
|
||||
serverProcess?.kill();
|
||||
serverProcess = null;
|
||||
if (await build()) {
|
||||
serverProcess = startServer();
|
||||
console.log("Restarted.\n");
|
||||
} else {
|
||||
console.error("Build failed, not restarting.\n");
|
||||
}
|
||||
}, WATCH_DEBOUNCE_MS);
|
||||
}
|
||||
} finally {
|
||||
watcher.close();
|
||||
serverProcess?.kill();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import { Config, defineConfig } from "drizzle-kit";
|
||||
|
||||
const dbUrl = Deno.env.get("POSTGRES_OPTIONS")
|
||||
const dbUrl = Deno.env.get("POSTGRES_OPTIONS");
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./db/schema.ts",
|
||||
@ -12,4 +12,4 @@ export default defineConfig({
|
||||
schemaFilter: ["public"],
|
||||
strict: true,
|
||||
verbose: true,
|
||||
});
|
||||
} as Config);
|
||||
|
||||
214
helpers.ts
214
helpers.ts
@ -28,54 +28,216 @@ export function getDaysInBaseMonth(year: number, month: number): number {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
|
||||
// Encryption functions (placeholders - these should be properly implemented)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Encryption: envelope format { salt, iv, data } (base64), AES-GCM + PBKDF2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PBKDF2_ITERATIONS = 100_000;
|
||||
const AES_KEY_LENGTH = 256;
|
||||
|
||||
/** Decode base64 to a new Uint8Array (ArrayBuffer-backed for Web Crypto). */
|
||||
function base64ToBytes(base64: string): Uint8Array {
|
||||
const bin = atob(base64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
let bin = "";
|
||||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
/** Decrypt AES-GCM ciphertext using key derived from password + salt. */
|
||||
async function decryptEnvelope(
|
||||
password: string,
|
||||
envelope: { salt: string; iv: string; data: string },
|
||||
): Promise<string> {
|
||||
const saltBytes = base64ToBytes(envelope.salt);
|
||||
const ivBytes = base64ToBytes(envelope.iv);
|
||||
const ciphertextBytes = base64ToBytes(envelope.data);
|
||||
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveKey"],
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: saltBytes as BufferSource,
|
||||
iterations: PBKDF2_ITERATIONS,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: AES_KEY_LENGTH },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: ivBytes as BufferSource },
|
||||
key,
|
||||
ciphertextBytes as BufferSource,
|
||||
);
|
||||
return new TextDecoder().decode(plaintext);
|
||||
}
|
||||
|
||||
/** True if the decoded payload is an encrypted envelope we can decrypt. */
|
||||
function isEnvelope(
|
||||
obj: unknown,
|
||||
): obj is { salt: string; iv: string; data: string } {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"salt" in obj &&
|
||||
"iv" in obj &&
|
||||
"data" in obj &&
|
||||
typeof (obj as { salt: unknown }).salt === "string" &&
|
||||
typeof (obj as { iv: unknown }).iv === "string" &&
|
||||
typeof (obj as { data: unknown }).data === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function encryptDataWithStoredPassword(
|
||||
_userId: string,
|
||||
data: string,
|
||||
): string {
|
||||
// TODO: Implement proper encryption
|
||||
// For now, return base64 encoded data as placeholder
|
||||
// Legacy: plain base64 for backward compatibility when no envelope is used
|
||||
return btoa(data);
|
||||
}
|
||||
|
||||
export function decryptDataWithStoredPassword(
|
||||
_userId: string,
|
||||
encryptedData: string,
|
||||
): string {
|
||||
// TODO: Implement proper decryption
|
||||
// For now, return base64 decoded data as placeholder
|
||||
try {
|
||||
return atob(encryptedData);
|
||||
} catch {
|
||||
return encryptedData; // Return as-is if not base64
|
||||
}
|
||||
}
|
||||
|
||||
export function tryDecryptDataWith(
|
||||
/**
|
||||
* Decrypt data using the PIN stored in localStorage for this user.
|
||||
* Supports: (1) encrypted envelope { salt, iv, data } then AES-GCM decrypt;
|
||||
* (2) legacy plain base64 string.
|
||||
*/
|
||||
export async function decryptDataWithStoredPassword(
|
||||
userId: string,
|
||||
encryptedData: string,
|
||||
): string | null {
|
||||
): Promise<string> {
|
||||
const password = getEncryptionPassword(userId);
|
||||
if (!password) {
|
||||
console.log(
|
||||
"[Encryption] decryptDataWithStoredPassword: no PIN for userId, skipping decrypt",
|
||||
);
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decryptDataWithStoredPassword(userId, encryptedData);
|
||||
const decoded = atob(encryptedData);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(decoded) as unknown;
|
||||
} catch {
|
||||
// Not JSON: treat as legacy plain base64 payload
|
||||
console.log(
|
||||
"[Encryption] decryptDataWithStoredPassword: legacy base64, returning decoded",
|
||||
);
|
||||
return decoded;
|
||||
}
|
||||
if (isEnvelope(parsed)) {
|
||||
const out = await decryptEnvelope(password, parsed);
|
||||
console.log(
|
||||
"[Encryption] decryptDataWithStoredPassword: envelope decrypted ok",
|
||||
);
|
||||
return out;
|
||||
}
|
||||
// JSON but not an envelope: legacy
|
||||
console.log(
|
||||
"[Encryption] decryptDataWithStoredPassword: JSON payload, returning as-is",
|
||||
);
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
console.log("[Encryption] decryptDataWithStoredPassword: failed", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrypt using stored PIN (from localStorage by userId). Returns null on failure. */
|
||||
export async function tryDecryptDataWith(
|
||||
userId: string,
|
||||
encryptedData: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const out = await decryptDataWithStoredPassword(userId, encryptedData);
|
||||
return out || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Decrypt using a raw PIN string (e.g. from server sync). Supports envelope or legacy base64. */
|
||||
export async function tryDecryptDataWithPin(
|
||||
pin: string,
|
||||
encryptedData: string,
|
||||
): Promise<string | null> {
|
||||
if (!pin) return null;
|
||||
try {
|
||||
const decoded = atob(encryptedData);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(decoded) as unknown;
|
||||
} catch {
|
||||
return decoded;
|
||||
}
|
||||
if (isEnvelope(parsed)) {
|
||||
return await decryptEnvelope(pin, parsed);
|
||||
}
|
||||
return decoded;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ENCRYPTION_KEY_PREFIX = "encryption_password_";
|
||||
|
||||
export function getEncryptionPassword(userId: string): string | null {
|
||||
// TODO: Implement proper password retrieval
|
||||
// For now, return null as placeholder
|
||||
const userKey = `encryption_password_${userId}`;
|
||||
if (!userId) {
|
||||
console.log("[Encryption] getEncryptionPassword: no userId");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return globalThis.localStorage?.getItem(userKey);
|
||||
const pin = globalThis.localStorage?.getItem(
|
||||
ENCRYPTION_KEY_PREFIX + userId,
|
||||
);
|
||||
console.log(
|
||||
"[Encryption] getEncryptionPassword:",
|
||||
pin ? "PIN available" : "no PIN",
|
||||
"key=" + ENCRYPTION_KEY_PREFIX + userId,
|
||||
);
|
||||
return pin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserEncryptionKey(_userId: string): string | null {
|
||||
// TODO: Implement proper key retrieval
|
||||
return null;
|
||||
/** Store encryption PIN in localStorage for this user (used by WelcomeBar and calendar). */
|
||||
export function setEncryptionPassword(
|
||||
userId: string,
|
||||
password: string,
|
||||
): boolean {
|
||||
if (!userId) {
|
||||
console.log("[Encryption] setEncryptionPassword: no userId, skip");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const key = getUserEncryptionKey(userId);
|
||||
if (key) globalThis.localStorage?.setItem(key, password);
|
||||
console.log("[Encryption] setEncryptionPassword: stored for key=" + key);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("[Encryption] setEncryptionPassword: failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserEncryptionKey(userId: string): string | null {
|
||||
if (!userId) return null;
|
||||
return ENCRYPTION_KEY_PREFIX + userId;
|
||||
}
|
||||
|
||||
export function getUserSkipKey(userId: string): string {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
getMonth,
|
||||
getYear,
|
||||
MakeNewDate,
|
||||
setEncryptionPassword,
|
||||
} from "../helpers.ts";
|
||||
import { Income } from "../services/financhalServices.ts";
|
||||
|
||||
@ -196,21 +197,67 @@ export default function FinancialCalendar(
|
||||
}
|
||||
|
||||
const processObligations = async () => {
|
||||
const encryptionPassword = getEncryptionPassword(userId);
|
||||
let encryptionPassword = getEncryptionPassword(userId);
|
||||
const hasEncrypted = obligations.some((o) => o.encrypted_data) ||
|
||||
incomes.some((i) => i.encrypted_data);
|
||||
console.log(
|
||||
"[Calendar] processObligations: userId=" +
|
||||
(userId ? "set" : "missing") + " pin=" +
|
||||
(encryptionPassword ? "available" : "missing") + " hasEncrypted=" +
|
||||
hasEncrypted,
|
||||
);
|
||||
|
||||
// If no local PIN but we have encrypted data, try to fetch PIN from server (e.g. new device or calendar opened first)
|
||||
if (!encryptionPassword && hasEncrypted) {
|
||||
try {
|
||||
console.log(
|
||||
"[Calendar] processObligations: fetching PIN from /api/sync-pin/get",
|
||||
);
|
||||
const res = await fetch("/api/sync-pin/get", {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.pin && setEncryptionPassword(userId, data.pin)) {
|
||||
encryptionPassword = data.pin;
|
||||
console.log(
|
||||
"[Calendar] processObligations: PIN stored from server, using for decrypt",
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"[Calendar] processObligations: server response ok but no pin or set failed",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Calendar] processObligations: sync-pin/get status=" +
|
||||
res.status,
|
||||
);
|
||||
}
|
||||
} catch (_e) {
|
||||
console.log(
|
||||
"[Calendar] processObligations: sync-pin/get fetch failed",
|
||||
_e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!encryptionPassword) {
|
||||
// No encryption key available, use obligations as-is
|
||||
console.log(
|
||||
"[Calendar] processObligations: no PIN, using obligations as-is",
|
||||
);
|
||||
dispatch({ type: "SET_PROCESSED_OBLIGATIONS", payload: obligations });
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedObligations: Obligation[] = [];
|
||||
let decryptedCount = 0;
|
||||
|
||||
for (const obligation of obligations) {
|
||||
const processedObligation = { ...obligation };
|
||||
|
||||
if (obligation.encrypted_data) {
|
||||
// Try to decrypt existing encrypted data
|
||||
try {
|
||||
const decryptedData = await decryptDataWithStoredPassword(
|
||||
userId,
|
||||
@ -218,28 +265,39 @@ export default function FinancialCalendar(
|
||||
);
|
||||
if (decryptedData) {
|
||||
const parsed = JSON.parse(decryptedData);
|
||||
console.log("[Calendar] decrypted obligation JSON:", parsed);
|
||||
if (parsed.amount !== undefined) {
|
||||
processedObligation.amount = parsed.amount;
|
||||
}
|
||||
if (parsed.description !== undefined) {
|
||||
processedObligation.description = parsed.description;
|
||||
}
|
||||
decryptedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt obligation data:", error);
|
||||
console.error(
|
||||
"[Calendar] Failed to decrypt obligation data:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updatedObligations.push(processedObligation);
|
||||
}
|
||||
|
||||
if (decryptedCount > 0) {
|
||||
console.log(
|
||||
"[Calendar] processObligations: decrypted " + decryptedCount +
|
||||
" obligation(s) with PIN",
|
||||
);
|
||||
}
|
||||
dispatch({
|
||||
type: "SET_PROCESSED_OBLIGATIONS",
|
||||
payload: updatedObligations,
|
||||
});
|
||||
};
|
||||
processObligations();
|
||||
}, [obligations, userId]);
|
||||
}, [obligations, incomes, userId]);
|
||||
|
||||
// Process incomes for decryption on component mount and when data changes
|
||||
useEffect(() => {
|
||||
@ -249,21 +307,62 @@ export default function FinancialCalendar(
|
||||
}
|
||||
|
||||
const processIncomes = async () => {
|
||||
const encryptionPassword = getEncryptionPassword(userId);
|
||||
let encryptionPassword = getEncryptionPassword(userId);
|
||||
const hasEncrypted = obligations.some((o) => o.encrypted_data) ||
|
||||
incomes.some((i) => i.encrypted_data);
|
||||
console.log(
|
||||
"[Calendar] processIncomes: userId=" + (userId ? "set" : "missing") +
|
||||
" pin=" + (encryptionPassword ? "available" : "missing") +
|
||||
" hasEncrypted=" + hasEncrypted,
|
||||
);
|
||||
|
||||
if (!encryptionPassword && hasEncrypted) {
|
||||
try {
|
||||
console.log(
|
||||
"[Calendar] processIncomes: fetching PIN from /api/sync-pin/get",
|
||||
);
|
||||
const res = await fetch("/api/sync-pin/get", {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.pin && setEncryptionPassword(userId, data.pin)) {
|
||||
encryptionPassword = data.pin;
|
||||
console.log(
|
||||
"[Calendar] processIncomes: PIN stored from server, using for decrypt",
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"[Calendar] processIncomes: server response ok but no pin or set failed",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Calendar] processIncomes: sync-pin/get status=" + res.status,
|
||||
);
|
||||
}
|
||||
} catch (_e) {
|
||||
console.log(
|
||||
"[Calendar] processIncomes: sync-pin/get fetch failed",
|
||||
_e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!encryptionPassword) {
|
||||
// No encryption key available, use incomes as-is
|
||||
console.log("[Calendar] processIncomes: no PIN, using incomes as-is");
|
||||
dispatch({ type: "SET_PROCESSED_INCOMES", payload: incomes });
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedIncomes: Income[] = [];
|
||||
let decryptedCount = 0;
|
||||
|
||||
for (const income of incomes) {
|
||||
const processedIncome = { ...income };
|
||||
|
||||
if (income.encrypted_data) {
|
||||
// Try to decrypt existing encrypted data
|
||||
try {
|
||||
const decryptedData = await decryptDataWithStoredPassword(
|
||||
userId,
|
||||
@ -271,26 +370,34 @@ export default function FinancialCalendar(
|
||||
);
|
||||
if (decryptedData) {
|
||||
const parsed = JSON.parse(decryptedData);
|
||||
console.log("[Calendar] decrypted income JSON:", parsed);
|
||||
if (parsed.amount !== undefined) {
|
||||
processedIncome.amount = parsed.amount;
|
||||
}
|
||||
if (parsed.source !== undefined) {
|
||||
processedIncome.source = parsed.source;
|
||||
}
|
||||
decryptedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt income data:", error);
|
||||
console.error("[Calendar] Failed to decrypt income data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
updatedIncomes.push(processedIncome);
|
||||
}
|
||||
|
||||
if (decryptedCount > 0) {
|
||||
console.log(
|
||||
"[Calendar] processIncomes: decrypted " + decryptedCount +
|
||||
" income(s) with PIN",
|
||||
);
|
||||
}
|
||||
dispatch({ type: "SET_PROCESSED_INCOMES", payload: updatedIncomes });
|
||||
};
|
||||
|
||||
processIncomes();
|
||||
}, [incomes, userId]);
|
||||
}, [incomes, obligations, userId]);
|
||||
|
||||
// Remove old form state variables - now managed in consolidated state
|
||||
const baseMonth = monthNum - 1;
|
||||
|
||||
@ -166,6 +166,54 @@ export default function ObligationList(
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleExportObligations() {
|
||||
const exportData = processedObligations.map((ob) => ({
|
||||
id: ob.id,
|
||||
description: ob.description ?? "",
|
||||
amount: ob.amount ?? 0,
|
||||
frequency: ob.frequency ?? "",
|
||||
due_date: ob.due_date
|
||||
? new Date(ob.due_date).toISOString().split("T")[0]
|
||||
: null,
|
||||
}));
|
||||
const json = JSON.stringify(exportData, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const suggestedName = `budget-obligations-${
|
||||
new Date().toISOString().split("T")[0]
|
||||
}.json`;
|
||||
|
||||
if (typeof globalThis.showSaveFilePicker === "function") {
|
||||
try {
|
||||
const handle = await globalThis.showSaveFilePicker({
|
||||
suggestedName,
|
||||
types: [{
|
||||
description: "JSON file",
|
||||
accept: { "application/json": [".json"] },
|
||||
}],
|
||||
});
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== "AbortError") {
|
||||
console.error("Export failed:", err);
|
||||
fallbackDownload(blob, suggestedName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fallbackDownload(blob, suggestedName);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackDownload(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{popupAddObligationDialog}
|
||||
@ -174,6 +222,13 @@ export default function ObligationList(
|
||||
<h2 class="theme-text list-title">
|
||||
Your Obligations
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="list-action-button list-action-button--center"
|
||||
onClick={handleExportObligations}
|
||||
>
|
||||
Export JSON
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="list-action-button"
|
||||
|
||||
@ -6,16 +6,6 @@ function applyTheme(root: Element, mode: string): void {
|
||||
root.setAttribute("data-theme-mode", mode);
|
||||
}
|
||||
|
||||
function readTheme(): string {
|
||||
try {
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function syncToggleLabels(root: Element): void {
|
||||
const isDark = root.getAttribute("data-theme-mode") !== "light";
|
||||
const label = isDark ? "Switch to light mode" : "Switch to dark mode";
|
||||
@ -26,9 +16,22 @@ function syncToggleLabels(root: Element): void {
|
||||
});
|
||||
}
|
||||
|
||||
const THEME_COOKIE = "cyberhybrid-theme";
|
||||
const COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
|
||||
|
||||
function setThemeCookie(mode: string): void {
|
||||
try {
|
||||
document.cookie =
|
||||
`${THEME_COOKIE}=${mode}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function persistTheme(mode: string): void {
|
||||
try {
|
||||
localStorage.setItem(THEME_KEY, mode);
|
||||
setThemeCookie(mode);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -42,7 +45,12 @@ export default function ThemeHydration() {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
applyTheme(root, readTheme());
|
||||
// Trust server-rendered theme (from cookie) on first paint; do not overwrite
|
||||
// with localStorage to avoid flicker. Sync storage to match so next load is correct.
|
||||
const serverTheme = root.getAttribute("data-theme-mode") === "light"
|
||||
? "light"
|
||||
: "dark";
|
||||
persistTheme(serverTheme);
|
||||
syncToggleLabels(root);
|
||||
|
||||
const handleClick = (event: Event) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { WelcomeBarProps, WelcomeBarState } from "./welcome-types.ts";
|
||||
import { useEncryption } from "./useEncryption.ts";
|
||||
import { useLogoutCleanup } from "./useServiceWorker.ts";
|
||||
import { useLogoutCleanup, useServiceWorker } from "./useServiceWorker.ts";
|
||||
import EncryptionModal from "./EncryptionModal.tsx";
|
||||
import NavigationMenu from "./NavigationMenu.tsx";
|
||||
import LockIcon from "./LockIcon.tsx";
|
||||
@ -46,6 +46,7 @@ export default function WelcomeBar(
|
||||
};
|
||||
|
||||
useLogoutCleanup();
|
||||
useServiceWorker();
|
||||
|
||||
useEffect(() => {
|
||||
const syncRoute = () => {
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
getUserEncryptionKey,
|
||||
getUserSkipKey,
|
||||
getUserSkipValue,
|
||||
tryDecryptDataWith,
|
||||
tryDecryptDataWithPin,
|
||||
} from "../../helpers.ts";
|
||||
|
||||
// Store encryption password in localStorage
|
||||
@ -92,6 +92,8 @@ export function useEncryption(
|
||||
const isMountedRef = useRef(true);
|
||||
// AbortController ref for cancelling fetch operations on unmount
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
// Ref for persist-across-devices so submit sees latest value (avoids stale state when user checks box then clicks Continue immediately)
|
||||
const persistAcrossDevicesRef = useRef(false);
|
||||
|
||||
// Consolidated encryption state
|
||||
const [state, setState] = useState({
|
||||
@ -146,6 +148,9 @@ export function useEncryption(
|
||||
// Check localStorage directly to see if password is already set
|
||||
const existingPassword = getEncryptionPassword(userId);
|
||||
if (existingPassword) {
|
||||
console.log(
|
||||
"[Encryption] useEncryption: local PIN already set for userId",
|
||||
);
|
||||
updateState({
|
||||
isPasswordSet: true,
|
||||
isSkipped: false,
|
||||
@ -167,6 +172,7 @@ export function useEncryption(
|
||||
if (isEncrypted) {
|
||||
const me = await fetch("/api/user/get", {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
@ -176,19 +182,24 @@ export function useEncryption(
|
||||
if (me.ok) {
|
||||
const userData = await me.json() as User;
|
||||
if (userData.id && userData.encrypted_data) {
|
||||
//console.log("User data found, checking local pin");
|
||||
console.log(
|
||||
"[Encryption] useEncryption: user has encrypted_data, checking local pin",
|
||||
);
|
||||
const localPin = getEncryptionPassword(userId);
|
||||
if (localPin) {
|
||||
//console.log("Local pin found, decrypting data");
|
||||
console.log(
|
||||
"[Encryption] useEncryption: local pin found, decrypting user data",
|
||||
);
|
||||
const decryptedData = await decryptDataWithStoredPassword(
|
||||
userId,
|
||||
userData.encrypted_data,
|
||||
);
|
||||
if (decryptedData) {
|
||||
const decryptedUserData = JSON.parse(decryptedData);
|
||||
//console.log("Checking user data", decryptedUserData);
|
||||
//console.log("user id ", userData.id);
|
||||
if (decryptedUserData.id === userData.id) {
|
||||
console.log(
|
||||
"[Encryption] useEncryption: local decrypt ok, user id match",
|
||||
);
|
||||
updateState({
|
||||
isPasswordSet: true,
|
||||
isSkipped: false,
|
||||
@ -196,11 +207,18 @@ export function useEncryption(
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Encryption] useEncryption: local decrypt returned empty",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//console.log("No local pin found, checking server");
|
||||
console.log(
|
||||
"[Encryption] useEncryption: no local pin, fetching from server",
|
||||
);
|
||||
const response = await fetch("/api/sync-pin/get", {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
@ -209,23 +227,28 @@ export function useEncryption(
|
||||
|
||||
if (response.ok) {
|
||||
const serverPinData = await response.json();
|
||||
//console.log("Server pin found, decrypting data", serverPinData);
|
||||
console.log(
|
||||
"[Encryption] useEncryption: server pin response ok",
|
||||
serverPinData.pin ? "pin present" : "no pin",
|
||||
);
|
||||
// Server has a pin, store it in localStorage for local usage
|
||||
if (serverPinData.pin) {
|
||||
const decryptedData = await tryDecryptDataWith(
|
||||
const decryptedData = await tryDecryptDataWithPin(
|
||||
serverPinData.pin,
|
||||
userData.encrypted_data,
|
||||
);
|
||||
if (decryptedData) {
|
||||
const decryptedUserData = JSON.parse(decryptedData);
|
||||
//console.log("Decrypted data found, checking user ID", decryptedUserData.id);
|
||||
//console.log("user id ", userData.id);
|
||||
if (decryptedUserData.id === userData.id) {
|
||||
console.log(
|
||||
"[Encryption] useEncryption: server pin decrypt ok, storing locally",
|
||||
);
|
||||
const localSetSuccess = setEncryptionPassword(
|
||||
userId,
|
||||
serverPinData.pin,
|
||||
);
|
||||
if (localSetSuccess) {
|
||||
persistAcrossDevicesRef.current = true;
|
||||
updateState({
|
||||
isPasswordSet: true,
|
||||
isSkipped: false,
|
||||
@ -235,6 +258,10 @@ export function useEncryption(
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Encryption] useEncryption: server pin decrypt failed or no user id match",
|
||||
);
|
||||
}
|
||||
|
||||
// Server pin was not usable for this encrypted data, fall back to asking user
|
||||
@ -336,6 +363,7 @@ export function useEncryption(
|
||||
//console.log(`[Encryption] Fetching user data for PIN handling`);
|
||||
const userResponse = await fetch("/api/user/get", {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
@ -390,6 +418,7 @@ export function useEncryption(
|
||||
const response = await fetch("/api/user/set", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@ -415,12 +444,14 @@ export function useEncryption(
|
||||
}
|
||||
}
|
||||
|
||||
// If persistAcrossDevices is true, sync to server
|
||||
if (state.persistAcrossDevices) {
|
||||
// If persist across devices was checked, sync to server (use ref so we see latest value even if user checked then clicked Continue immediately)
|
||||
const shouldPersist = persistAcrossDevicesRef.current;
|
||||
if (shouldPersist) {
|
||||
try {
|
||||
const response = await fetch("/api/sync-pin/set", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@ -435,6 +466,7 @@ export function useEncryption(
|
||||
}
|
||||
} catch (_serverError) {
|
||||
// Still allow local usage even if server sync fails
|
||||
persistAcrossDevicesRef.current = false;
|
||||
updateState({
|
||||
passwordError: "PIN set locally but failed to sync across devices",
|
||||
persistAcrossDevices: false,
|
||||
@ -486,8 +518,9 @@ export function useEncryption(
|
||||
}
|
||||
};
|
||||
|
||||
// Handle opening change PIN modal
|
||||
// Handle opening change PIN modal (sync ref from state so submit uses current persist choice)
|
||||
const handleChangePin = () => {
|
||||
persistAcrossDevicesRef.current = state.persistAcrossDevices;
|
||||
updateState({
|
||||
showChangePinModal: true,
|
||||
showRemovePinConfirm: false,
|
||||
@ -523,6 +556,7 @@ export function useEncryption(
|
||||
const _response = await fetch("/api/sync-pin/delete", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@ -584,12 +618,14 @@ export function useEncryption(
|
||||
throw new Error("Failed to store PIN locally");
|
||||
}
|
||||
|
||||
// If persistAcrossDevices is true, sync to server
|
||||
if (state.persistAcrossDevices) {
|
||||
// If persist across devices is checked, sync to server (use ref for same-tick checkbox + submit)
|
||||
const shouldPersist = persistAcrossDevicesRef.current;
|
||||
if (shouldPersist) {
|
||||
try {
|
||||
const response = await fetch("/api/sync-pin/set", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@ -608,6 +644,7 @@ export function useEncryption(
|
||||
}
|
||||
} catch (_serverError) {
|
||||
// Still allow local usage even if server sync fails
|
||||
persistAcrossDevicesRef.current = false;
|
||||
updateState({
|
||||
passwordError:
|
||||
"PIN updated locally but failed to sync across devices",
|
||||
@ -628,8 +665,9 @@ export function useEncryption(
|
||||
}
|
||||
};
|
||||
|
||||
// Handle persist across devices checkbox change
|
||||
// Handle persist across devices checkbox change (ref updated synchronously so submit sees it even if user clicks Continue immediately)
|
||||
const handlePersistAcrossDevicesChange = (persist: boolean) => {
|
||||
persistAcrossDevicesRef.current = persist;
|
||||
updateState({ persistAcrossDevices: persist });
|
||||
};
|
||||
|
||||
|
||||
@ -32,88 +32,40 @@ function safeSessionStorage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage Service Worker registration
|
||||
* Handles registration, unregistration, and session-based updates
|
||||
* Includes robust error handling for network issues and broken pipes
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* useServiceWorker();
|
||||
* return <div>Component content</div>;
|
||||
* }
|
||||
* ```
|
||||
* Ensures no Service Worker is used (temporary).
|
||||
* Unregisters all service workers and verifies none remain.
|
||||
*/
|
||||
export function useServiceWorker() {
|
||||
useEffect(() => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
// Check if this is a new user experience (session start)
|
||||
const storage = safeSessionStorage();
|
||||
const isNewSession = !storage?.getItem("sw_registered");
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
|
||||
// Add timeout for service worker operations
|
||||
// const registrationTimeout = setTimeout(() => {
|
||||
// console.warn("Service Worker registration taking longer than expected");
|
||||
// }, 10000);
|
||||
|
||||
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||
const activeSW = registrations.find((reg) =>
|
||||
reg.active?.scriptURL.includes("fetch_sw.js")
|
||||
);
|
||||
|
||||
if (activeSW && !isNewSession) {
|
||||
// Service worker already active and not a new session - skip registration
|
||||
// console.log("Service Worker already active.");
|
||||
// clearTimeout(registrationTimeout);
|
||||
return;
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then((registrations) => {
|
||||
if (registrations.length === 0) {
|
||||
console.log("[SW] No service workers registered (OK).");
|
||||
return [] as ServiceWorkerRegistration[];
|
||||
}
|
||||
|
||||
// New session or no active SW - unregister old versions and register new
|
||||
const unregisterPromises = registrations.map((registration) =>
|
||||
registration.unregister().catch((_err) => {
|
||||
// Log but don't fail if unregistration fails
|
||||
// console.warn("Failed to unregister old service worker:", err);
|
||||
return Promise.resolve();
|
||||
const unregisterPromises = registrations.map((reg) =>
|
||||
reg.unregister().then(() => {
|
||||
console.log("[SW] Unregistered:", reg.scope);
|
||||
}).catch((err) => {
|
||||
console.warn("[SW] Unregister failed for", reg.scope, err);
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(unregisterPromises).then(() => {
|
||||
// Register the new service worker after unregistering old ones
|
||||
return navigator.serviceWorker.register("/fetch_sw.js");
|
||||
}).then(() => {
|
||||
// console.log("Service Worker registered.");
|
||||
const swStorage = safeSessionStorage();
|
||||
swStorage?.setItem("sw_registered", "true");
|
||||
// clearTimeout(registrationTimeout);
|
||||
}).catch((_regError) => {
|
||||
// Handle registration errors gracefully
|
||||
// console.error("Service Worker registration failed:", regError);
|
||||
// clearTimeout(registrationTimeout);
|
||||
// Don't throw - allow app to continue without service worker
|
||||
});
|
||||
}).catch((_error) => {
|
||||
//clearTimeout(registrationTimeout);
|
||||
|
||||
// Handle different error types
|
||||
// const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// if (error instanceof TypeError || errorMessage.includes("network")) {
|
||||
// console.error("Network error during service worker setup:", error);
|
||||
// } else if (errorMessage.includes("broken pipe")) {
|
||||
// console.error("Connection broken during service worker setup:", error);
|
||||
// } else {
|
||||
// console.error("Service Worker registration failed:", error);
|
||||
// }
|
||||
// App should continue functioning without service worker
|
||||
});
|
||||
return Promise.all(unregisterPromises).then(() =>
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
);
|
||||
})
|
||||
.then((after) => {
|
||||
if (after && after.length > 0) {
|
||||
console.warn("[SW] Still registered after unregister:", after.length);
|
||||
} else {
|
||||
console.log("[SW] Verified: no service workers registered.");
|
||||
}
|
||||
// else {
|
||||
// console.log("Service Worker not supported in this browser.");
|
||||
// }
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
// No cleanup needed for service worker registration
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[SW] Check failed:", err);
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
|
||||
@ -29,8 +29,10 @@ export default define.page(async function App(props: PageProps) {
|
||||
(now.getMonth() + 1).toString().padStart(2, "0")
|
||||
}`;
|
||||
|
||||
const themeMode = state.themePreference ?? "dark";
|
||||
|
||||
return (
|
||||
<html data-theme-mode="dark">
|
||||
<html data-theme-mode={themeMode}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@ -84,8 +84,8 @@ export const handler = define.handlers({
|
||||
|
||||
await setOnboardingStep(user_id, 2);
|
||||
|
||||
return new Response("", {
|
||||
status: 302,
|
||||
return new Response(null, {
|
||||
status: 303,
|
||||
headers: { Location: "/landlord/onboarding/property-setup" },
|
||||
});
|
||||
},
|
||||
|
||||
@ -65,9 +65,7 @@ export function InitiPGSessionMiddleware() {
|
||||
const url = new URL(ctx.req.url);
|
||||
|
||||
console.log("Pathname:", ctx.req.method, url.pathname);
|
||||
if (url.hostname === "cyberhybridhub.com") {
|
||||
return Response.redirect("https://portal.cyberhybridhub.com", 302);
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.startsWith("/api/ai-images/file/") ||
|
||||
url.pathname.startsWith("/api/audio-blob/") ||
|
||||
@ -188,6 +186,11 @@ export function InitiPGSessionMiddleware() {
|
||||
ctx.state.stateId = sessionCookieParams.stateId ?? "";
|
||||
ctx.state.sealKey = theStateKey ?? undefined; // Store the validated seal key for logout
|
||||
|
||||
const themeCookie = getCookies(ctx.req.headers)["cyberhybrid-theme"];
|
||||
if (themeCookie === "light" || themeCookie === "dark") {
|
||||
ctx.state.themePreference = themeCookie;
|
||||
}
|
||||
|
||||
// Inject full user data into ctx.state.user for route handlers
|
||||
let userData: User | null = null;
|
||||
if (sessionData && sessionData.user_id) {
|
||||
|
||||
0
setup-postregs-db.sh
Normal file → Executable file
0
setup-postregs-db.sh
Normal file → Executable file
@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /home/nathan/nathanAndrinoid/cyberhybridhub.com
|
||||
cd /home/chh/cyberhybridhub.com
|
||||
deno task dev
|
||||
|
||||
@ -81,53 +81,5 @@ self.addEventListener("message", async (event) => {
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
if (event.request.method === "GET" && url.pathname === "/api/sync-pin/get") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(event.request);
|
||||
|
||||
if (cachedResponse) {
|
||||
const clonedCache = cachedResponse.clone();
|
||||
const cachedText = await clonedCache.text();
|
||||
const cachedHash = await hashText(cachedText);
|
||||
|
||||
// Create a new URL with naunce parameter (don't modify the original)
|
||||
const checkUrl = new URL(event.request.url);
|
||||
checkUrl.searchParams.set("naunce", cachedHash);
|
||||
|
||||
const checkResponse = await fetch(checkUrl.toString(), {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (checkResponse.ok) {
|
||||
const cloneResponse = checkResponse.clone();
|
||||
const cloneText = await cloneResponse.text();
|
||||
|
||||
if (cloneText && cloneText.length > 0) {
|
||||
await cache.put(event.request, checkResponse.clone());
|
||||
return checkResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(event.request, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (networkResponse.ok) {
|
||||
await cache.put(event.request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
})(),
|
||||
);
|
||||
}
|
||||
// For all other requests, fall back to the network without cloning the body
|
||||
event.respondWith(fetch(event.request));
|
||||
// For all other non-API requests, let them handle by default browser behavior
|
||||
});
|
||||
|
||||
2
utils.ts
2
utils.ts
@ -20,6 +20,8 @@ export interface State {
|
||||
user?: User | null;
|
||||
auth?: AuthState;
|
||||
authError?: string;
|
||||
/** Theme for initial HTML to avoid flash; set from cookie in middleware. */
|
||||
themePreference?: "light" | "dark";
|
||||
}
|
||||
|
||||
export type PageProps<Data = unknown> = FreshPageProps<Data, State>;
|
||||
|
||||
@ -83,13 +83,11 @@ const EDGE_ALLOWED_ORIGINS = (() => {
|
||||
})();
|
||||
|
||||
const ISLAND_ALLOWED_ORIGINS = new Set([
|
||||
"https://studymylanguage.com",
|
||||
"http://studymylanguage.com",
|
||||
"https://data1.cyberhybridhub.com",
|
||||
"http://data1.cyberhybridhub.com",
|
||||
"https://cyberhybridhub.com",
|
||||
"http://cyberhybridhub.com",
|
||||
"http://localhost:8080",
|
||||
"http://96.3.221.225",
|
||||
"https://96.3.221.225",
|
||||
"http://96.3.218.128",
|
||||
"https://96.3.218.126",
|
||||
]);
|
||||
|
||||
function mergeVary(current: string | null, incoming: string): string {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user