Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ These are alternatives to Dashy's built-in auth, Keycloak, and OIDC. Most of the

- [Reverse Proxy Auth](#reverse-proxy-auth) - Authelia, Authentik, or similar sitting in front of Dashy
- [Zero-Trust Tunnels](#zero-trust-tunnels) - Cloudflare Tunnel, Tailscale Funnel
- [Service Worker & Offline Use](#service-worker--offline-use) - Staying logged in with the service worker enabled
- [VPN](#vpn) - Keep Dashy off the internet entirely
- [IP-Based Access](#ip-based-access) - Restrict by source IP in your web server
- [Web Server Authentication](#web-server-authentication) - HTTP basic auth at the proxy level
Expand Down Expand Up @@ -726,6 +727,10 @@ These let you expose Dashy to the internet without opening inbound ports or conf

**Tailscale Funnel** exposes Dashy through your Tailscale mesh to the public internet, with automatic TLS. Simpler to set up than Cloudflare but you get less control over access policies. See the [Funnel docs](https://tailscale.com/kb/1223/funnel).

### Service worker & offline use

If you run Dashy behind any of the redirect-based proxies above and also enable the service worker for offline use (`appConfig.enableServiceWorker: true`), set `appConfig.enableAuthProxyCompat: true` as well. Without it, when your proxy session expires the cached app can get stuck — the service worker keeps serving the old page instead of letting the proxy redirect you to its login screen. With it enabled, Dashy detects the expiry on load and reloads so you can sign in again.

### VPN

A VPN keeps Dashy off the public internet entirely. You connect to your home network remotely and access Dashy like you're on the LAN. No auth to configure, no attack surface to worry about. The downside: you need the VPN running to see anything, and some networks (corporate WiFi, hotels) block VPN traffic.
Expand Down
4 changes: 4 additions & 0 deletions docs/authentication/cloudflare-tunnel.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ Solution: The email Cloudflare sends must exactly match a `users[].user` entry w
Problem: Each refresh bounces back through Cloudflare's login.<br>
Solution: Session cookies are being blocked or stripped. Check that the tunnel's public hostname uses HTTPS (it does by default), that no upstream proxy strips third-party cookies, and that the Access application's session duration is non-zero.

#### Stuck on a cached page after the Access session expires
Problem: You have the service worker enabled (`appConfig.enableServiceWorker`), and once your Access session expires Dashy keeps showing a broken cached page instead of redirecting to the Cloudflare login.<br>
Solution: Set `appConfig.enableAuthProxyCompat: true`. Dashy will then detect the expiry on load and reload so Cloudflare can redirect you to log in again.

#### "Critical Configuration Load Error" on first load
Problem: SPA errors out before showing the dashboard.<br>
Solution: Usually a CORS or network reachability issue, but for Cloudflare Tunnel this is most often a `cloudflared` to `dashy` networking problem inside Docker. `docker compose logs cloudflared` will show connection attempts. Confirm both services are on the same Docker network and that the tunnel's public hostname URL matches the service name and port (e.g. `dashy:8080`, not `localhost:8080`).
Expand Down
1 change: 1 addition & 0 deletions docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ For more info, see the[Multi-Page docs](/docs/pages-and-sections.md#multi-page-s
**`disableSmartSort`** | `boolean` | _Optional_ | For the most-used and last-used app sort functions to work, a basic open-count is stored in local storage. If you do not want this to happen, then disable smart sort here, but you wil no longer be able to use these sort options. Defaults to `false`.
**`disableUpdateChecks`** | `boolean` | _Optional_ | If set to true, Dashy will not check for updates. Defaults to `false`.
**`enableServiceWorker`** | `boolean` | _Optional_ | Service workers cache web applications to improve load times and offer basic offline functionality, and are disabled by default in Dashy. The service worker can sometimes cause older content to be cached, requiring the app to be hard-refreshed. If you do not want SW functionality, or are having issues with caching, set this property to `false` to disable all service workers.
**`enableAuthProxyCompat`** | `boolean` | _Optional_ | Only relevant when `enableServiceWorker` is `true` and when Dashy sits behind an authentication proxy (such as Cloudflare Zero Trust or Authelia). Set this to `true` to detect that redirect on load, unregister the service worker, and reload so re-authentication can proceed. Defaults to `false`.
**`disableContextMenu`** | `boolean` | _Optional_ | If set to `true`, the custom right-click context menu will be disabled. Defaults to `false`.

**[⬆️ Back to Top](#configuring)**
Expand Down
1 change: 1 addition & 0 deletions services/auth-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ function maybeBootstrapConfig(filePath, opts) {
appConfig: {
auth: full.appConfig?.auth || {},
enableServiceWorker: full.appConfig?.enableServiceWorker,
enableAuthProxyCompat: full.appConfig?.enableAuthProxyCompat,
},
pageInfo: { title: `Login | ${full.pageInfo?.title || 'Dashy'}` },
});
Expand Down
50 changes: 49 additions & 1 deletion src/utils/InitServiceWorker.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { load as yamlLoad } from 'js-yaml';
import request from '@/utils/request';
import i18n from '@/utils/i18n';
import { serviceEndpoints } from '@/utils/config/defaults';
import { statusMsg, statusErrorMsg } from '@/utils/logging/CoolConsole';
import { toast } from '@/utils/Toast';

const SW_LABEL = 'Service Worker Status';
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000; // hourly
const AUTH_PROXY_COMPAT_KEY = 'dashy-auth-proxy-compat'; // mirrors appConfig.enableAuthProxyCompat
const AUTH_PROXY_RELOAD_KEY = 'dashy-auth-proxy-reloaded'; // one reload per tab, guards against loops

/* Loads conf.yml and returns the parsed object, or null on failure */
const loadAppConfig = async () => {
Expand All @@ -28,6 +31,44 @@ const unregisterAll = async () => {
} catch { /* no-op */ }
};

/* Remember the opt-in so it's known on later loads even when conf.yml comes from cache or fails */
const rememberAuthProxyCompat = (enabled) => {
try {
if (enabled) localStorage.setItem(AUTH_PROXY_COMPAT_KEY, '1');
else localStorage.removeItem(AUTH_PROXY_COMPAT_KEY);
} catch { /* storage unavailable */ }
};

const isReloadGuardSet = () => {
try { return !!sessionStorage.getItem(AUTH_PROXY_RELOAD_KEY); } catch { return true; }
};
const setReloadGuard = (on) => {
try {
if (on) sessionStorage.setItem(AUTH_PROXY_RELOAD_KEY, '1');
else sessionStorage.removeItem(AUTH_PROXY_RELOAD_KEY);
} catch { /* no-op */ }
};

/**
* If enabled (with appConfig.enableAuthProxyCompat), then check for expired sessions,
* and then drop the cached SW and trigger a reload so the proxy can re-authenticate user
*/
const recoverFromAuthProxy = async () => {
try { if (localStorage.getItem(AUTH_PROXY_COMPAT_KEY) !== '1') return false; } catch { return false; }
let res;
try {
res = await fetch(serviceEndpoints.getUser, { cache: 'no-store', redirect: 'manual' });
} catch { return false; } // network error / offline - keep SW for offline use
if (res.type !== 'opaqueredirect') { setReloadGuard(false); return false; } // valid session - re-arm
if (!navigator.serviceWorker.controller) return false; // no cached SW blocking the redirect
if (isReloadGuardSet()) return false; // already reloaded once - avoid repeat on false positives
setReloadGuard(true);
statusMsg(SW_LABEL, 'Auth proxy redirect detected - unregistering SW and reloading.');
await unregisterAll();
window.location.reload();
return true;
};

/* Sticky toast with a Refresh action that swaps in the new SW and reloads */
const promptForUpdate = (updateSW) => {
const t = i18n.global.t;
Expand All @@ -44,7 +85,14 @@ const initServiceWorker = async () => {
if (!('serviceWorker' in navigator)) return;

const conf = await loadAppConfig();
if (!conf) return; // network/parse failed — leave any existing SW alone
if (conf?.appConfig) {
rememberAuthProxyCompat(conf.appConfig.enableServiceWorker && conf.appConfig.enableAuthProxyCompat);
}

// Probe for an auth-proxy session expiry
if (await recoverFromAuthProxy()) return;

if (!conf) return; // network/parse failed - leave any existing SW alone

if (!conf.appConfig?.enableServiceWorker) {
await unregisterAll();
Expand Down
6 changes: 6 additions & 0 deletions src/utils/config/ConfigSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,12 @@
"default": false,
"description": "If set to true, then service workers will be used to cache page contents"
},
"enableAuthProxyCompat": {
"title": "Enable Auth Proxy Compatibility",
"type": "boolean",
"default": false,
"description": "If set to true, when an authentication proxy (e.g. Cloudflare Zero Trust, Authelia) redirects after session expiry, the service worker is unregistered and the page reloaded so the login redirect can proceed. Only relevant when enableServiceWorker is true"
},
"disableContextMenu": {
"title": "Disable Context Menus",
"type": "boolean",
Expand Down